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.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.
273 hostname = nic.hostname
275 hostname = nic.machine.name
281 return hostname + '.' + config.dns.domains[0]
283 def getNicInfo(data_dict, machine):
284 """Helper function for info, get data on nics for a machine.
286 Modifies data_dict to include the relevant data, and returns a list
287 of (key, name) pairs to display "name: data_dict[key]" to the user.
289 data_dict['num_nics'] = len(machine.nics)
290 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
291 ('nic%s_mac', 'NIC %s MAC Addr'),
292 ('nic%s_ip', 'NIC %s IP'),
295 for i in range(len(machine.nics)):
296 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
297 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
298 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
299 data_dict['nic%s_ip' % i] = machine.nics[i].ip
300 if len(machine.nics) == 1:
301 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
304 def getDiskInfo(data_dict, machine):
305 """Helper function for info, get data on disks for a machine.
307 Modifies data_dict to include the relevant data, and returns a list
308 of (key, name) pairs to display "name: data_dict[key]" to the user.
310 data_dict['num_disks'] = len(machine.disks)
311 disk_fields_template = [('%s_size', '%s size')]
313 for disk in machine.disks:
314 name = disk.guest_device_name
315 disk_fields.extend([(x % name, y % name) for x, y in
316 disk_fields_template])
317 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
320 def command(username, state, path, fields):
321 """Handler for running commands like boot and delete on a VM."""
322 back = fields.getfirst('back')
324 d = controls.commandResult(username, state, fields)
325 if d['command'] == 'Delete VM':
327 except InvalidInput, err:
330 print >> sys.stderr, err
335 return templates.command(searchList=[d])
337 state.clear() #Changed global state
338 d = getListDict(username, state)
340 return templates.list(searchList=[d])
342 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
343 return ({'Status': '303 See Other',
344 'Location': 'info?machine_id=%d' % machine.machine_id},
345 "You shouldn't see this message.")
347 raise InvalidInput('back', back, 'Not a known back page.')
349 def modifyDict(username, state, fields):
350 """Modify a machine as specified by CGI arguments.
352 Return a list of local variables for modify.tmpl.
357 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
358 validate = validation.Validate(username, state, **kws)
359 machine = validate.machine
360 oldname = machine.name
362 if hasattr(validate, 'memory'):
363 machine.memory = validate.memory
365 if hasattr(validate, 'vmtype'):
366 machine.type = validate.vmtype
368 if hasattr(validate, 'disksize'):
369 disksize = validate.disksize
370 disk = machine.disks[0]
371 if disk.size != disksize:
372 olddisk[disk.guest_device_name] = disksize
374 session.save_or_update(disk)
377 if hasattr(validate, 'owner') and validate.owner != machine.owner:
378 machine.owner = validate.owner
380 if hasattr(validate, 'name'):
381 machine.name = validate.name
382 for n in machine.nics:
383 if n.hostname == oldname:
384 n.hostname = validate.name
385 if hasattr(validate, 'description'):
386 machine.description = validate.description
387 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
388 machine.administrator = validate.admin
390 if hasattr(validate, 'contact'):
391 machine.contact = validate.contact
393 session.save_or_update(machine)
395 cache_acls.refreshMachine(machine)
400 for diskname in olddisk:
401 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
402 if hasattr(validate, 'name'):
403 controls.renameMachine(machine, oldname, validate.name)
404 return dict(user=username,
408 def modify(username, state, path, fields):
409 """Handler for modifying attributes of a machine."""
411 modify_dict = modifyDict(username, state, fields)
412 except InvalidInput, err:
414 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
416 machine = modify_dict['machine']
419 info_dict = infoDict(username, state, machine)
420 info_dict['err'] = err
422 for field in fields.keys():
423 setattr(info_dict['defaults'], field, fields.getfirst(field))
424 info_dict['result'] = result
425 return templates.info(searchList=[info_dict])
428 def helpHandler(username, state, path, fields):
429 """Handler for help messages."""
430 simple = fields.getfirst('simple')
431 subjects = fields.getlist('subject')
435 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
436 ParaVM. You can access the resulting system by logging into the <a
437 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
438 with your Kerberos tickets; there is no root password so sshd will
441 <p>Under the covers, the autoinstaller uses our own patched version of
442 xen-create-image, which is a tool based on debootstrap. If you log
443 into the serial console while the install is running, you can watch
446 'ParaVM Console': """
447 ParaVM machines do not support local console access over VNC. To
448 access the serial console of these machines, you can SSH with Kerberos
449 to %s, using the name of the machine as your
450 username.""" % config.console.hostname,
452 HVM machines use the virtualization features of the processor, while
453 ParaVM machines rely on a modified kernel to communicate directly with
454 the hypervisor. HVMs support boot CDs of any operating system, and
455 the VNC console applet. The three-minute autoinstaller produces
456 ParaVMs. ParaVMs typically are more efficient, and always support the
457 <a href="help?subject=ParaVM+Console">console server</a>.</p>
459 <p>More details are <a
460 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
461 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
462 (which you can skip by using the autoinstaller to begin with.)</p>
464 <p>We recommend using a ParaVM when possible and an HVM when necessary.
467 Don't ask us! We're as mystified as you are.""",
469 The owner field is used to determine <a
470 href="help?subject=Quotas">quotas</a>. It must be the name of a
471 locker that you are an AFS administrator of. In particular, you or an
472 AFS group you are a member of must have AFS rlidwka bits on the
473 locker. You can check who administers the LOCKER locker using the
474 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
475 href="help?subject=Administrator">administrator</a>.""",
477 The administrator field determines who can access the console and
478 power on and off the machine. This can be either a user or a moira
481 Quotas are determined on a per-locker basis. Each locker may have a
482 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
485 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
486 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
487 your machine will run just fine, but the applet's display of the
488 console will suffer artifacts.
491 <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>
492 <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.
497 subjects = sorted(help_mapping.keys())
499 d = dict(user=username,
502 mapping=help_mapping)
504 return templates.help(searchList=[d])
507 def badOperation(u, s, p, e):
508 """Function called when accessing an unknown URI."""
509 return ({'Status': '404 Not Found'}, 'Invalid operation.')
511 def infoDict(username, state, machine):
512 """Get the variables used by info.tmpl."""
513 status = controls.statusInfo(machine)
514 checkpoint.checkpoint('Getting status info')
515 has_vnc = hasVnc(status)
517 main_status = dict(name=machine.name,
518 memory=str(machine.memory))
522 main_status = dict(status[1:])
523 main_status['host'] = controls.listHost(machine)
524 start_time = float(main_status.get('start_time', 0))
525 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
526 cpu_time_float = float(main_status.get('cpu_time', 0))
527 cputime = datetime.timedelta(seconds=int(cpu_time_float))
528 checkpoint.checkpoint('Status')
529 display_fields = """name uptime memory state cpu_weight on_reboot
530 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
531 display_fields = [('name', 'Name'),
532 ('description', 'Description'),
534 ('administrator', 'Administrator'),
535 ('contact', 'Contact'),
538 ('uptime', 'uptime'),
539 ('cputime', 'CPU usage'),
540 ('host', 'Hosted on'),
543 ('state', 'state (xen format)'),
544 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
545 ('on_reboot', 'Action on VM reboot'),
546 ('on_poweroff', 'Action on VM poweroff'),
547 ('on_crash', 'Action on VM crash'),
548 ('on_xend_start', 'Action on Xen start'),
549 ('on_xend_stop', 'Action on Xen stop'),
550 ('bootloader', 'Bootloader options'),
554 machine_info['name'] = machine.name
555 machine_info['description'] = machine.description
556 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
557 machine_info['owner'] = machine.owner
558 machine_info['administrator'] = machine.administrator
559 machine_info['contact'] = machine.contact
561 nic_fields = getNicInfo(machine_info, machine)
562 nic_point = display_fields.index('NIC_INFO')
563 display_fields = (display_fields[:nic_point] + nic_fields +
564 display_fields[nic_point+1:])
566 disk_fields = getDiskInfo(machine_info, machine)
567 disk_point = display_fields.index('DISK_INFO')
568 display_fields = (display_fields[:disk_point] + disk_fields +
569 display_fields[disk_point+1:])
571 main_status['memory'] += ' MiB'
572 for field, disp in display_fields:
573 if field in ('uptime', 'cputime') and locals()[field] is not None:
574 fields.append((disp, locals()[field]))
575 elif field in machine_info:
576 fields.append((disp, machine_info[field]))
577 elif field in main_status:
578 fields.append((disp, main_status[field]))
581 #fields.append((disp, None))
583 checkpoint.checkpoint('Got fields')
586 max_mem = validation.maxMemory(machine.owner, state, machine, False)
587 checkpoint.checkpoint('Got mem')
588 max_disk = validation.maxDisk(machine.owner, machine)
589 defaults = Defaults()
590 for name in 'machine_id name description administrator owner memory contact'.split():
591 setattr(defaults, name, getattr(machine, name))
592 defaults.type = machine.type.type_id
593 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
594 checkpoint.checkpoint('Got defaults')
595 d = dict(user=username,
596 on=status is not None,
604 owner_help=helppopup("Owner"),
608 def info(username, state, path, fields):
609 """Handler for info on a single VM."""
610 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
611 d = infoDict(username, state, machine)
612 checkpoint.checkpoint('Got infodict')
613 return templates.info(searchList=[d])
615 def unauthFront(_, _2, _3, fields):
616 """Information for unauth'd users."""
617 return templates.unauth(searchList=[{'simple' : True}])
619 def admin(username, state, path, fields):
621 return ({'Status': '303 See Other',
622 'Location': 'admin/'},
623 "You shouldn't see this message.")
624 if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
625 raise InvalidInput('username', username,
626 'Not in admin group %s.' % config.web.adminacl)
627 newstate = State(username, isadmin=True)
628 newstate.environ = state.environ
629 return handler(username, newstate, path, fields)
631 def throwError(_, __, ___, ____):
632 """Throw an error, to test the error-tracing mechanisms."""
633 raise RuntimeError("test of the emergency broadcast system")
635 mapping = dict(list=listVms,
645 errortest=throwError)
647 def printHeaders(headers):
648 """Print a dictionary as HTTP headers."""
649 for key, value in headers.iteritems():
650 print '%s: %s' % (key, value)
653 def send_error_mail(subject, body):
656 to = config.web.errormail
662 """ % (to, config.web.hostname, subject, body)
663 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
664 stdin=subprocess.PIPE)
669 def show_error(op, username, fields, err, emsg, traceback):
670 """Print an error page when an exception occurs"""
671 d = dict(op=op, user=username, fields=fields,
672 errorMessage=str(err), stderr=emsg, traceback=traceback)
673 details = templates.error_raw(searchList=[d])
674 exclude = config.web.errormail_exclude
675 if username not in exclude and '*' not in exclude:
676 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
678 d['details'] = details
679 return templates.error(searchList=[d])
681 def getUser(environ):
682 """Return the current user based on the SSL environment variables"""
683 user = environ.get('REMOTE_USER')
687 if environ.get('AUTH_TYPE') == 'Negotiate':
688 # Convert the krb5 principal into a krb4 username
689 if not user.endswith('@%s' % config.kerberos.realm):
692 return user.split('@')[0].replace('/', '.')
696 def handler(username, state, path, fields):
697 operation, path = pathSplit(path)
700 print 'Starting', operation
701 fun = mapping.get(operation, badOperation)
702 return fun(username, state, path, fields)
705 def __init__(self, environ, start_response):
706 self.environ = environ
707 self.start = start_response
709 self.username = getUser(environ)
710 self.state = State(self.username)
711 self.state.environ = environ
716 start_time = time.time()
717 database.clear_cache()
718 sys.stderr = StringIO()
719 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
720 operation = self.environ.get('PATH_INFO', '')
722 self.start("301 Moved Permanently", [('Location', './')])
724 if self.username is None:
728 checkpoint.checkpoint('Before')
729 output = handler(self.username, self.state, operation, fields)
730 checkpoint.checkpoint('After')
732 headers = dict(DEFAULT_HEADERS)
733 if isinstance(output, tuple):
734 new_headers, output = output
735 headers.update(new_headers)
736 e = revertStandardError()
738 if hasattr(output, 'addError'):
741 # This only happens on redirects, so it'd be a pain to get
742 # the message to the user. Maybe in the response is useful.
743 output = output + '\n\nstderr:\n' + e
744 output_string = str(output)
745 checkpoint.checkpoint('output as a string')
746 except Exception, err:
747 if not fields.has_key('js'):
748 if isinstance(err, InvalidInput):
749 self.start('200 OK', [('Content-Type', 'text/html')])
750 e = revertStandardError()
751 yield str(invalidInput(operation, self.username, fields,
755 self.start('500 Internal Server Error',
756 [('Content-Type', 'text/html')])
757 e = revertStandardError()
758 s = show_error(operation, self.username, fields,
759 err, e, traceback.format_exc())
762 status = headers.setdefault('Status', '200 OK')
763 del headers['Status']
764 self.start(status, headers.items())
766 if fields.has_key('timedebug'):
767 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
774 from flup.server.fcgi_fork import WSGIServer
775 WSGIServer(constructor()).run()
777 if __name__ == '__main__':