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,
206 checkpoint.checkpoint('Got defaults')
207 def sortkey(machine):
208 return (machine.owner != username, machine.owner, machine.name)
209 machines = sorted(machines, key=sortkey)
210 d = dict(user=username,
211 cant_add_vm=validation.cantAddVm(username, state),
212 max_memory=max_memory,
220 def listVms(username, state, path, fields):
221 """Handler for list requests."""
222 checkpoint.checkpoint('Getting list dict')
223 d = getListDict(username, state)
224 checkpoint.checkpoint('Got list dict')
225 return templates.list(searchList=[d])
227 def vnc(username, state, path, fields):
230 Note that due to same-domain restrictions, the applet connects to
231 the webserver, which needs to forward those requests to the xen
232 server. The Xen server runs another proxy that (1) authenticates
233 and (2) finds the correct port for the VM.
235 You might want iptables like:
237 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
238 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
239 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
240 --dport 10003 -j SNAT --to-source 18.187.7.142
241 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
242 --dport 10003 -j ACCEPT
244 Remember to enable iptables!
245 echo 1 > /proc/sys/net/ipv4/ip_forward
247 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
249 token = controls.vnctoken(machine)
250 host = controls.listHost(machine)
252 port = 10003 + [h.hostname for h in config.hosts].index(host)
256 status = controls.statusInfo(machine)
257 has_vnc = hasVnc(status)
259 d = dict(user=username,
263 hostname=state.environ.get('SERVER_NAME', 'localhost'),
266 return templates.vnc(searchList=[d])
268 def getHostname(nic):
269 """Find the hostname associated with a NIC.
271 XXX this should be merged with the similar logic in DNS and DHCP.
273 if nic.hostname and '.' in nic.hostname:
276 return nic.machine.name + '.' + config.dns.domains[0]
281 def getNicInfo(data_dict, machine):
282 """Helper function for info, get data on nics for a machine.
284 Modifies data_dict to include the relevant data, and returns a list
285 of (key, name) pairs to display "name: data_dict[key]" to the user.
287 data_dict['num_nics'] = len(machine.nics)
288 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
289 ('nic%s_mac', 'NIC %s MAC Addr'),
290 ('nic%s_ip', 'NIC %s IP'),
293 for i in range(len(machine.nics)):
294 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
296 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
297 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
298 data_dict['nic%s_ip' % i] = machine.nics[i].ip
299 if len(machine.nics) == 1:
300 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
303 def getDiskInfo(data_dict, machine):
304 """Helper function for info, get data on disks for a machine.
306 Modifies data_dict to include the relevant data, and returns a list
307 of (key, name) pairs to display "name: data_dict[key]" to the user.
309 data_dict['num_disks'] = len(machine.disks)
310 disk_fields_template = [('%s_size', '%s size')]
312 for disk in machine.disks:
313 name = disk.guest_device_name
314 disk_fields.extend([(x % name, y % name) for x, y in
315 disk_fields_template])
316 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
319 def command(username, state, path, fields):
320 """Handler for running commands like boot and delete on a VM."""
321 back = fields.getfirst('back')
323 d = controls.commandResult(username, state, fields)
324 if d['command'] == 'Delete VM':
326 except InvalidInput, err:
329 print >> sys.stderr, err
334 return templates.command(searchList=[d])
336 state.clear() #Changed global state
337 d = getListDict(username, state)
339 return templates.list(searchList=[d])
341 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
342 return ({'Status': '303 See Other',
343 'Location': 'info?machine_id=%d' % machine.machine_id},
344 "You shouldn't see this message.")
346 raise InvalidInput('back', back, 'Not a known back page.')
348 def modifyDict(username, state, fields):
349 """Modify a machine as specified by CGI arguments.
351 Return a list of local variables for modify.tmpl.
356 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
357 validate = validation.Validate(username, state, **kws)
358 machine = validate.machine
359 oldname = machine.name
361 if hasattr(validate, 'memory'):
362 machine.memory = validate.memory
364 if hasattr(validate, 'vmtype'):
365 machine.type = validate.vmtype
367 if hasattr(validate, 'disksize'):
368 disksize = validate.disksize
369 disk = machine.disks[0]
370 if disk.size != disksize:
371 olddisk[disk.guest_device_name] = disksize
373 session.save_or_update(disk)
376 if hasattr(validate, 'owner') and validate.owner != machine.owner:
377 machine.owner = validate.owner
379 if hasattr(validate, 'name'):
380 machine.name = validate.name
381 if hasattr(validate, 'description'):
382 machine.description = validate.description
383 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
384 machine.administrator = validate.admin
386 if hasattr(validate, 'contact'):
387 machine.contact = validate.contact
389 session.save_or_update(machine)
391 cache_acls.refreshMachine(machine)
396 for diskname in olddisk:
397 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
398 if hasattr(validate, 'name'):
399 controls.renameMachine(machine, oldname, validate.name)
400 return dict(user=username,
404 def modify(username, state, path, fields):
405 """Handler for modifying attributes of a machine."""
407 modify_dict = modifyDict(username, state, fields)
408 except InvalidInput, err:
410 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
412 machine = modify_dict['machine']
415 info_dict = infoDict(username, state, machine)
416 info_dict['err'] = err
418 for field in fields.keys():
419 setattr(info_dict['defaults'], field, fields.getfirst(field))
420 info_dict['result'] = result
421 return templates.info(searchList=[info_dict])
424 def helpHandler(username, state, path, fields):
425 """Handler for help messages."""
426 simple = fields.getfirst('simple')
427 subjects = fields.getlist('subject')
431 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
432 ParaVM. You can access the resulting system by logging into the <a
433 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
434 with your Kerberos tickets; there is no root password so sshd will
437 <p>Under the covers, the autoinstaller uses our own patched version of
438 xen-create-image, which is a tool based on debootstrap. If you log
439 into the serial console while the install is running, you can watch
442 'ParaVM Console': """
443 ParaVM machines do not support local console access over VNC. To
444 access the serial console of these machines, you can SSH with Kerberos
445 to %s, using the name of the machine as your
446 username.""" % config.console.hostname,
448 HVM machines use the virtualization features of the processor, while
449 ParaVM machines rely on a modified kernel to communicate directly with
450 the hypervisor. HVMs support boot CDs of any operating system, and
451 the VNC console applet. The three-minute autoinstaller produces
452 ParaVMs. ParaVMs typically are more efficient, and always support the
453 <a href="help?subject=ParaVM+Console">console server</a>.</p>
455 <p>More details are <a
456 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
457 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
458 (which you can skip by using the autoinstaller to begin with.)</p>
460 <p>We recommend using a ParaVM when possible and an HVM when necessary.
463 Don't ask us! We're as mystified as you are.""",
465 The owner field is used to determine <a
466 href="help?subject=Quotas">quotas</a>. It must be the name of a
467 locker that you are an AFS administrator of. In particular, you or an
468 AFS group you are a member of must have AFS rlidwka bits on the
469 locker. You can check who administers the LOCKER locker using the
470 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
471 href="help?subject=Administrator">administrator</a>.""",
473 The administrator field determines who can access the console and
474 power on and off the machine. This can be either a user or a moira
477 Quotas are determined on a per-locker basis. Each locker may have a
478 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
481 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
482 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
483 your machine will run just fine, but the applet's display of the
484 console will suffer artifacts.
487 <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>
488 <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.
493 subjects = sorted(help_mapping.keys())
495 d = dict(user=username,
498 mapping=help_mapping)
500 return templates.help(searchList=[d])
503 def badOperation(u, s, p, e):
504 """Function called when accessing an unknown URI."""
505 return ({'Status': '404 Not Found'}, 'Invalid operation.')
507 def infoDict(username, state, machine):
508 """Get the variables used by info.tmpl."""
509 status = controls.statusInfo(machine)
510 checkpoint.checkpoint('Getting status info')
511 has_vnc = hasVnc(status)
513 main_status = dict(name=machine.name,
514 memory=str(machine.memory))
518 main_status = dict(status[1:])
519 main_status['host'] = controls.listHost(machine)
520 start_time = float(main_status.get('start_time', 0))
521 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
522 cpu_time_float = float(main_status.get('cpu_time', 0))
523 cputime = datetime.timedelta(seconds=int(cpu_time_float))
524 checkpoint.checkpoint('Status')
525 display_fields = """name uptime memory state cpu_weight on_reboot
526 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
527 display_fields = [('name', 'Name'),
528 ('description', 'Description'),
530 ('administrator', 'Administrator'),
531 ('contact', 'Contact'),
534 ('uptime', 'uptime'),
535 ('cputime', 'CPU usage'),
536 ('host', 'Hosted on'),
539 ('state', 'state (xen format)'),
540 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
541 ('on_reboot', 'Action on VM reboot'),
542 ('on_poweroff', 'Action on VM poweroff'),
543 ('on_crash', 'Action on VM crash'),
544 ('on_xend_start', 'Action on Xen start'),
545 ('on_xend_stop', 'Action on Xen stop'),
546 ('bootloader', 'Bootloader options'),
550 machine_info['name'] = machine.name
551 machine_info['description'] = machine.description
552 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
553 machine_info['owner'] = machine.owner
554 machine_info['administrator'] = machine.administrator
555 machine_info['contact'] = machine.contact
557 nic_fields = getNicInfo(machine_info, machine)
558 nic_point = display_fields.index('NIC_INFO')
559 display_fields = (display_fields[:nic_point] + nic_fields +
560 display_fields[nic_point+1:])
562 disk_fields = getDiskInfo(machine_info, machine)
563 disk_point = display_fields.index('DISK_INFO')
564 display_fields = (display_fields[:disk_point] + disk_fields +
565 display_fields[disk_point+1:])
567 main_status['memory'] += ' MiB'
568 for field, disp in display_fields:
569 if field in ('uptime', 'cputime') and locals()[field] is not None:
570 fields.append((disp, locals()[field]))
571 elif field in machine_info:
572 fields.append((disp, machine_info[field]))
573 elif field in main_status:
574 fields.append((disp, main_status[field]))
577 #fields.append((disp, None))
579 checkpoint.checkpoint('Got fields')
582 max_mem = validation.maxMemory(machine.owner, state, machine, False)
583 checkpoint.checkpoint('Got mem')
584 max_disk = validation.maxDisk(machine.owner, machine)
585 defaults = Defaults()
586 for name in 'machine_id name description administrator owner memory contact'.split():
587 setattr(defaults, name, getattr(machine, name))
588 defaults.type = machine.type.type_id
589 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
590 checkpoint.checkpoint('Got defaults')
591 d = dict(user=username,
592 on=status is not None,
600 owner_help=helppopup("Owner"),
604 def info(username, state, path, fields):
605 """Handler for info on a single VM."""
606 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
607 d = infoDict(username, state, machine)
608 checkpoint.checkpoint('Got infodict')
609 return templates.info(searchList=[d])
611 def unauthFront(_, _2, _3, fields):
612 """Information for unauth'd users."""
613 return templates.unauth(searchList=[{'simple' : True}])
615 def admin(username, state, path, fields):
617 return ({'Status': '303 See Other',
618 'Location': 'admin/'},
619 "You shouldn't see this message.")
620 if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
621 raise InvalidInput('username', username,
622 'Not in admin group %s.' % config.web.adminacl)
623 newstate = State(username, isadmin=True)
624 newstate.environ = state.environ
625 return handler(username, newstate, path, fields)
627 def throwError(_, __, ___, ____):
628 """Throw an error, to test the error-tracing mechanisms."""
629 raise RuntimeError("test of the emergency broadcast system")
631 mapping = dict(list=listVms,
641 errortest=throwError)
643 def printHeaders(headers):
644 """Print a dictionary as HTTP headers."""
645 for key, value in headers.iteritems():
646 print '%s: %s' % (key, value)
649 def send_error_mail(subject, body):
652 to = config.web.errormail
658 """ % (to, config.web.hostname, subject, body)
659 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
660 stdin=subprocess.PIPE)
665 def show_error(op, username, fields, err, emsg, traceback):
666 """Print an error page when an exception occurs"""
667 d = dict(op=op, user=username, fields=fields,
668 errorMessage=str(err), stderr=emsg, traceback=traceback)
669 details = templates.error_raw(searchList=[d])
670 exclude = config.web.errormail_exclude
671 if username not in exclude and '*' not in exclude:
672 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
674 d['details'] = details
675 return templates.error(searchList=[d])
677 def getUser(environ):
678 """Return the current user based on the SSL environment variables"""
679 user = environ.get('REMOTE_USER')
683 if environ.get('AUTH_TYPE') == 'Negotiate':
684 # Convert the krb5 principal into a krb4 username
685 if not user.endswith('@%s' % config.authn[0].realm):
688 return user.split('@')[0].replace('/', '.')
692 def handler(username, state, path, fields):
693 operation, path = pathSplit(path)
696 print 'Starting', operation
697 fun = mapping.get(operation, badOperation)
698 return fun(username, state, path, fields)
701 def __init__(self, environ, start_response):
702 self.environ = environ
703 self.start = start_response
705 self.username = getUser(environ)
706 self.state = State(self.username)
707 self.state.environ = environ
712 start_time = time.time()
713 database.clear_cache()
714 sys.stderr = StringIO()
715 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
716 operation = self.environ.get('PATH_INFO', '')
718 self.start("301 Moved Permanently", [('Location', './')])
720 if self.username is None:
724 checkpoint.checkpoint('Before')
725 output = handler(self.username, self.state, operation, fields)
726 checkpoint.checkpoint('After')
728 headers = dict(DEFAULT_HEADERS)
729 if isinstance(output, tuple):
730 new_headers, output = output
731 headers.update(new_headers)
732 e = revertStandardError()
734 if hasattr(output, 'addError'):
737 # This only happens on redirects, so it'd be a pain to get
738 # the message to the user. Maybe in the response is useful.
739 output = output + '\n\nstderr:\n' + e
740 output_string = str(output)
741 checkpoint.checkpoint('output as a string')
742 except Exception, err:
743 if not fields.has_key('js'):
744 if isinstance(err, InvalidInput):
745 self.start('200 OK', [('Content-Type', 'text/html')])
746 e = revertStandardError()
747 yield str(invalidInput(operation, self.username, fields,
751 self.start('500 Internal Server Error',
752 [('Content-Type', 'text/html')])
753 e = revertStandardError()
754 s = show_error(operation, self.username, fields,
755 err, e, traceback.format_exc())
758 status = headers.setdefault('Status', '200 OK')
759 del headers['Status']
760 self.start(status, headers.items())
762 if fields.has_key('timedebug'):
763 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
770 from flup.server.fcgi_fork import WSGIServer
771 WSGIServer(constructor()).run()
773 if __name__ == '__main__':