2 """Main CGI script for web interface"""
15 from StringIO import StringIO
17 def revertStandardError():
18 """Move stderr to stdout, and return the contents of the old stderr."""
20 if not isinstance(errio, StringIO):
22 sys.stderr = sys.stdout
27 """Revert stderr to stdout, and print the contents of stderr"""
28 if isinstance(sys.stderr, StringIO):
29 print revertStandardError()
31 if __name__ == '__main__':
33 atexit.register(printError)
36 from Cheetah.Template import Template
39 from webcommon import State
41 from getafsgroups import getAfsGroupMembers
42 from invirt import database
43 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
44 from invirt.config import structs as config
45 from invirt.common import InvalidInput, CodeError
48 if path.startswith('/'):
53 return path[:i], path[i:]
57 self.start_time = time.time()
60 def checkpoint(self, s):
61 self.checkpoints.append((s, time.time()))
64 return ('Timing info:\n%s\n' %
65 '\n'.join(['%s: %s' % (d, t - self.start_time) for
66 (d, t) in self.checkpoints]))
68 checkpoint = Checkpoint()
71 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
74 """Return HTML code for a (?) link to a specified help topic"""
75 return ('<span class="helplink"><a href="help?' +
76 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
77 +'" target="_blank" ' +
78 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
80 def makeErrorPre(old, addition):
84 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
86 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
88 Template.database = database
89 Template.config = config
90 Template.helppopup = staticmethod(helppopup)
94 """Class to store a dictionary that will be converted to JSON"""
95 def __init__(self, **kws):
103 return simplejson.dumps(self.data)
105 def addError(self, text):
106 """Add stderr text to be displayed on the website."""
108 makeErrorPre(self.data.get('err'), text)
111 """Class to store default values for fields."""
120 def __init__(self, max_memory=None, max_disk=None, **kws):
121 if max_memory is not None:
122 self.memory = min(self.memory, max_memory)
123 if max_disk is not None:
124 self.max_disk = min(self.disk, max_disk)
126 setattr(self, key, kws[key])
130 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
132 def invalidInput(op, username, fields, err, emsg):
133 """Print an error page when an InvalidInput exception occurs"""
134 d = dict(op=op, user=username, err_field=err.err_field,
135 err_value=str(err.err_value), stderr=emsg,
136 errorMessage=str(err))
137 return templates.invalid(searchList=[d])
140 """Does the machine with a given status list support VNC?"""
144 if l[0] == 'device' and l[1][0] == 'vfb':
146 return 'location' in d
149 def parseCreate(username, state, fields):
150 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
151 validate = validation.Validate(username, state, strict=True, **kws)
152 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
153 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
154 cdrom=getattr(validate, 'cdrom', None),
155 autoinstall=getattr(validate, 'autoinstall', None))
157 def create(username, state, path, fields):
158 """Handler for create requests."""
160 parsed_fields = parseCreate(username, state, fields)
161 machine = controls.createVm(username, state, **parsed_fields)
162 except InvalidInput, err:
166 state.clear() #Changed global state
167 d = getListDict(username, state)
170 for field in fields.keys():
171 setattr(d['defaults'], field, fields.getfirst(field))
173 d['new_machine'] = parsed_fields['name']
174 return templates.list(searchList=[d])
177 def getListDict(username, state):
178 """Gets the list of local variables used by list.tmpl."""
179 checkpoint.checkpoint('Starting')
180 machines = state.machines
181 checkpoint.checkpoint('Got my machines')
184 xmlist = state.xmlist
185 checkpoint.checkpoint('Got uptimes')
186 can_clone = 'ice3' not in state.xmlist_raw
192 m.uptime = xmlist[m]['uptime']
193 if xmlist[m]['console']:
198 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
199 max_memory = validation.maxMemory(username, state)
200 max_disk = validation.maxDisk(username)
201 checkpoint.checkpoint('Got max mem/disk')
202 defaults = Defaults(max_memory=max_memory,
205 checkpoint.checkpoint('Got defaults')
206 def sortkey(machine):
207 return (machine.owner != username, machine.owner, machine.name)
208 machines = sorted(machines, key=sortkey)
209 d = dict(user=username,
210 cant_add_vm=validation.cantAddVm(username, state),
211 max_memory=max_memory,
219 def listVms(username, state, path, fields):
220 """Handler for list requests."""
221 checkpoint.checkpoint('Getting list dict')
222 d = getListDict(username, state)
223 checkpoint.checkpoint('Got list dict')
224 return templates.list(searchList=[d])
226 def vnc(username, state, path, fields):
229 Note that due to same-domain restrictions, the applet connects to
230 the webserver, which needs to forward those requests to the xen
231 server. The Xen server runs another proxy that (1) authenticates
232 and (2) finds the correct port for the VM.
234 You might want iptables like:
236 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
237 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
238 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
239 --dport 10003 -j SNAT --to-source 18.187.7.142
240 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
241 --dport 10003 -j ACCEPT
243 Remember to enable iptables!
244 echo 1 > /proc/sys/net/ipv4/ip_forward
246 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
248 token = controls.vnctoken(machine)
249 host = controls.listHost(machine)
251 port = 10003 + [h.hostname for h in config.hosts].index(host)
255 status = controls.statusInfo(machine)
256 has_vnc = hasVnc(status)
258 d = dict(user=username,
262 hostname=state.environ.get('SERVER_NAME', 'localhost'),
265 return templates.vnc(searchList=[d])
267 def getHostname(nic):
268 """Find the hostname associated with a NIC.
270 XXX this should be merged with the similar logic in DNS and DHCP.
272 if nic.hostname and '.' in nic.hostname:
275 return nic.machine.name + '.' + config.dns.domains[0]
280 def getNicInfo(data_dict, machine):
281 """Helper function for info, get data on nics for a machine.
283 Modifies data_dict to include the relevant data, and returns a list
284 of (key, name) pairs to display "name: data_dict[key]" to the user.
286 data_dict['num_nics'] = len(machine.nics)
287 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
288 ('nic%s_mac', 'NIC %s MAC Addr'),
289 ('nic%s_ip', 'NIC %s IP'),
292 for i in range(len(machine.nics)):
293 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
295 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
296 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
297 data_dict['nic%s_ip' % i] = machine.nics[i].ip
298 if len(machine.nics) == 1:
299 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
302 def getDiskInfo(data_dict, machine):
303 """Helper function for info, get data on disks for a machine.
305 Modifies data_dict to include the relevant data, and returns a list
306 of (key, name) pairs to display "name: data_dict[key]" to the user.
308 data_dict['num_disks'] = len(machine.disks)
309 disk_fields_template = [('%s_size', '%s size')]
311 for disk in machine.disks:
312 name = disk.guest_device_name
313 disk_fields.extend([(x % name, y % name) for x, y in
314 disk_fields_template])
315 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
318 def command(username, state, path, fields):
319 """Handler for running commands like boot and delete on a VM."""
320 back = fields.getfirst('back')
322 d = controls.commandResult(username, state, fields)
323 if d['command'] == 'Delete VM':
325 except InvalidInput, err:
328 print >> sys.stderr, err
333 return templates.command(searchList=[d])
335 state.clear() #Changed global state
336 d = getListDict(username, state)
338 return templates.list(searchList=[d])
340 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
341 return ({'Status': '303 See Other',
342 'Location': 'info?machine_id=%d' % machine.machine_id},
343 "You shouldn't see this message.")
345 raise InvalidInput('back', back, 'Not a known back page.')
347 def modifyDict(username, state, fields):
348 """Modify a machine as specified by CGI arguments.
350 Return a list of local variables for modify.tmpl.
355 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
356 validate = validation.Validate(username, state, **kws)
357 machine = validate.machine
358 oldname = machine.name
360 if hasattr(validate, 'memory'):
361 machine.memory = validate.memory
363 if hasattr(validate, 'vmtype'):
364 machine.type = validate.vmtype
366 if hasattr(validate, 'disksize'):
367 disksize = validate.disksize
368 disk = machine.disks[0]
369 if disk.size != disksize:
370 olddisk[disk.guest_device_name] = disksize
372 session.save_or_update(disk)
375 if hasattr(validate, 'owner') and validate.owner != machine.owner:
376 machine.owner = validate.owner
378 if hasattr(validate, 'name'):
379 machine.name = validate.name
380 if hasattr(validate, 'description'):
381 machine.description = validate.description
382 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
383 machine.administrator = validate.admin
385 if hasattr(validate, 'contact'):
386 machine.contact = validate.contact
388 session.save_or_update(machine)
390 cache_acls.refreshMachine(machine)
395 for diskname in olddisk:
396 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
397 if hasattr(validate, 'name'):
398 controls.renameMachine(machine, oldname, validate.name)
399 return dict(user=username,
403 def modify(username, state, path, fields):
404 """Handler for modifying attributes of a machine."""
406 modify_dict = modifyDict(username, state, fields)
407 except InvalidInput, err:
409 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
411 machine = modify_dict['machine']
414 info_dict = infoDict(username, state, machine)
415 info_dict['err'] = err
417 for field in fields.keys():
418 setattr(info_dict['defaults'], field, fields.getfirst(field))
419 info_dict['result'] = result
420 return templates.info(searchList=[info_dict])
423 def helpHandler(username, state, path, fields):
424 """Handler for help messages."""
425 simple = fields.getfirst('simple')
426 subjects = fields.getlist('subject')
430 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
431 ParaVM. You can access the resulting system by logging into the <a
432 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
433 with your Kerberos tickets; there is no root password so sshd will
436 <p>Under the covers, the autoinstaller uses our own patched version of
437 xen-create-image, which is a tool based on debootstrap. If you log
438 into the serial console while the install is running, you can watch
441 'ParaVM Console': """
442 ParaVM machines do not support local console access over VNC. To
443 access the serial console of these machines, you can SSH with Kerberos
444 to %s, using the name of the machine as your
445 username.""" % config.console.hostname,
447 HVM machines use the virtualization features of the processor, while
448 ParaVM machines rely on a modified kernel to communicate directly with
449 the hypervisor. HVMs support boot CDs of any operating system, and
450 the VNC console applet. The three-minute autoinstaller produces
451 ParaVMs. ParaVMs typically are more efficient, and always support the
452 <a href="help?subject=ParaVM+Console">console server</a>.</p>
454 <p>More details are <a
455 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
456 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
457 (which you can skip by using the autoinstaller to begin with.)</p>
459 <p>We recommend using a ParaVM when possible and an HVM when necessary.
462 Don't ask us! We're as mystified as you are.""",
464 The owner field is used to determine <a
465 href="help?subject=Quotas">quotas</a>. It must be the name of a
466 locker that you are an AFS administrator of. In particular, you or an
467 AFS group you are a member of must have AFS rlidwka bits on the
468 locker. You can check who administers the LOCKER locker using the
469 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
470 href="help?subject=Administrator">administrator</a>.""",
472 The administrator field determines who can access the console and
473 power on and off the machine. This can be either a user or a moira
476 Quotas are determined on a per-locker basis. Each locker may have a
477 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
480 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
481 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
482 your machine will run just fine, but the applet's display of the
483 console will suffer artifacts.
486 <strong>Windows Vista:</strong> The Vista image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-email.txt">the licensing confirmation e-mail</a> for details. The installer req uires 512 MB RAM and at least 7.5 GB disk space (15 GB or more recommended).<br>
487 <strong>Windows XP:</strong> This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one.
492 subjects = sorted(help_mapping.keys())
494 d = dict(user=username,
497 mapping=help_mapping)
499 return templates.help(searchList=[d])
502 def badOperation(u, s, p, e):
503 """Function called when accessing an unknown URI."""
504 return ({'Status': '404 Not Found'}, 'Invalid operation.')
506 def infoDict(username, state, machine):
507 """Get the variables used by info.tmpl."""
508 status = controls.statusInfo(machine)
509 checkpoint.checkpoint('Getting status info')
510 has_vnc = hasVnc(status)
512 main_status = dict(name=machine.name,
513 memory=str(machine.memory))
517 main_status = dict(status[1:])
518 main_status['host'] = controls.listHost(machine)
519 start_time = float(main_status.get('start_time', 0))
520 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
521 cpu_time_float = float(main_status.get('cpu_time', 0))
522 cputime = datetime.timedelta(seconds=int(cpu_time_float))
523 checkpoint.checkpoint('Status')
524 display_fields = """name uptime memory state cpu_weight on_reboot
525 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
526 display_fields = [('name', 'Name'),
527 ('description', 'Description'),
529 ('administrator', 'Administrator'),
530 ('contact', 'Contact'),
533 ('uptime', 'uptime'),
534 ('cputime', 'CPU usage'),
535 ('host', 'Hosted on'),
538 ('state', 'state (xen format)'),
539 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
540 ('on_reboot', 'Action on VM reboot'),
541 ('on_poweroff', 'Action on VM poweroff'),
542 ('on_crash', 'Action on VM crash'),
543 ('on_xend_start', 'Action on Xen start'),
544 ('on_xend_stop', 'Action on Xen stop'),
545 ('bootloader', 'Bootloader options'),
549 machine_info['name'] = machine.name
550 machine_info['description'] = machine.description
551 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
552 machine_info['owner'] = machine.owner
553 machine_info['administrator'] = machine.administrator
554 machine_info['contact'] = machine.contact
556 nic_fields = getNicInfo(machine_info, machine)
557 nic_point = display_fields.index('NIC_INFO')
558 display_fields = (display_fields[:nic_point] + nic_fields +
559 display_fields[nic_point+1:])
561 disk_fields = getDiskInfo(machine_info, machine)
562 disk_point = display_fields.index('DISK_INFO')
563 display_fields = (display_fields[:disk_point] + disk_fields +
564 display_fields[disk_point+1:])
566 main_status['memory'] += ' MiB'
567 for field, disp in display_fields:
568 if field in ('uptime', 'cputime') and locals()[field] is not None:
569 fields.append((disp, locals()[field]))
570 elif field in machine_info:
571 fields.append((disp, machine_info[field]))
572 elif field in main_status:
573 fields.append((disp, main_status[field]))
576 #fields.append((disp, None))
578 checkpoint.checkpoint('Got fields')
581 max_mem = validation.maxMemory(machine.owner, state, machine, False)
582 checkpoint.checkpoint('Got mem')
583 max_disk = validation.maxDisk(machine.owner, machine)
584 defaults = Defaults()
585 for name in 'machine_id name description administrator owner memory contact'.split():
586 setattr(defaults, name, getattr(machine, name))
587 defaults.type = machine.type.type_id
588 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
589 checkpoint.checkpoint('Got defaults')
590 d = dict(user=username,
591 on=status is not None,
599 owner_help=helppopup("Owner"),
603 def info(username, state, path, fields):
604 """Handler for info on a single VM."""
605 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
606 d = infoDict(username, state, machine)
607 checkpoint.checkpoint('Got infodict')
608 return templates.info(searchList=[d])
610 def unauthFront(_, _2, _3, fields):
611 """Information for unauth'd users."""
612 return templates.unauth(searchList=[{'simple' : True}])
614 def admin(username, state, path, fields):
616 return ({'Status': '303 See Other',
617 'Location': 'admin/'},
618 "You shouldn't see this message.")
619 if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
620 raise InvalidInput('username', username,
621 'Not in admin group %s.' % config.web.adminacl)
622 newstate = State(username, isadmin=True)
623 newstate.environ = state.environ
624 return handler(username, newstate, path, fields)
626 def throwError(_, __, ___, ____):
627 """Throw an error, to test the error-tracing mechanisms."""
628 raise RuntimeError("test of the emergency broadcast system")
630 mapping = dict(list=listVms,
640 errortest=throwError)
642 def printHeaders(headers):
643 """Print a dictionary as HTTP headers."""
644 for key, value in headers.iteritems():
645 print '%s: %s' % (key, value)
648 def send_error_mail(subject, body):
651 to = config.web.errormail
657 """ % (to, config.web.hostname, subject, body)
658 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
659 stdin=subprocess.PIPE)
664 def show_error(op, username, fields, err, emsg, traceback):
665 """Print an error page when an exception occurs"""
666 d = dict(op=op, user=username, fields=fields,
667 errorMessage=str(err), stderr=emsg, traceback=traceback)
668 details = templates.error_raw(searchList=[d])
669 exclude = config.web.errormail_exclude
670 if username not in exclude and '*' not in exclude:
671 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
673 d['details'] = details
674 return templates.error(searchList=[d])
676 def getUser(environ):
677 """Return the current user based on the SSL environment variables"""
678 user = environ.get('REMOTE_USER')
682 if environ.get('AUTH_TYPE') == 'Negotiate':
683 # Convert the krb5 principal into a krb4 username
684 if not user.endswith('@%s' % config.kerberos.realm):
687 return user.split('@')[0].replace('/', '.')
691 def handler(username, state, path, fields):
692 operation, path = pathSplit(path)
695 print 'Starting', operation
696 fun = mapping.get(operation, badOperation)
697 return fun(username, state, path, fields)
700 def __init__(self, environ, start_response):
701 self.environ = environ
702 self.start = start_response
704 self.username = getUser(environ)
705 self.state = State(self.username)
706 self.state.environ = environ
711 start_time = time.time()
712 database.clear_cache()
713 sys.stderr = StringIO()
714 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
715 operation = self.environ.get('PATH_INFO', '')
717 self.start("301 Moved Permanently", [('Location', './')])
719 if self.username is None:
723 checkpoint.checkpoint('Before')
724 output = handler(self.username, self.state, operation, fields)
725 checkpoint.checkpoint('After')
727 headers = dict(DEFAULT_HEADERS)
728 if isinstance(output, tuple):
729 new_headers, output = output
730 headers.update(new_headers)
731 e = revertStandardError()
733 if hasattr(output, 'addError'):
736 # This only happens on redirects, so it'd be a pain to get
737 # the message to the user. Maybe in the response is useful.
738 output = output + '\n\nstderr:\n' + e
739 output_string = str(output)
740 checkpoint.checkpoint('output as a string')
741 except Exception, err:
742 if not fields.has_key('js'):
743 if isinstance(err, InvalidInput):
744 self.start('200 OK', [('Content-Type', 'text/html')])
745 e = revertStandardError()
746 yield str(invalidInput(operation, self.username, fields,
750 self.start('500 Internal Server Error',
751 [('Content-Type', 'text/html')])
752 e = revertStandardError()
753 s = show_error(operation, self.username, fields,
754 err, e, traceback.format_exc())
757 status = headers.setdefault('Status', '200 OK')
758 del headers['Status']
759 self.start(status, headers.items())
761 if fields.has_key('timedebug'):
762 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
769 from flup.server.fcgi_fork import WSGIServer
770 WSGIServer(constructor()).run()
772 if __name__ == '__main__':