2 """Main CGI script for web interface"""
17 from StringIO import StringIO
18 def revertStandardError():
19 """Move stderr to stdout, and return the contents of the old stderr."""
21 if not isinstance(errio, StringIO):
23 sys.stderr = sys.stdout
28 """Revert stderr to stdout, and print the contents of stderr"""
29 if isinstance(sys.stderr, StringIO):
30 print revertStandardError()
32 if __name__ == '__main__':
34 atexit.register(printError)
37 from Cheetah.Template import Template
40 from webcommon import State
42 from getafsgroups import getAfsGroupMembers
43 from invirt import database
44 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
45 from invirt.config import structs as config
46 from invirt.common import InvalidInput, CodeError
50 class InvirtWeb(View):
52 super(self.__class__,self).__init__()
60 if path.startswith('/'):
65 return path[:i], path[i:]
69 self.start_time = time.time()
72 def checkpoint(self, s):
73 self.checkpoints.append((s, time.time()))
76 return ('Timing info:\n%s\n' %
77 '\n'.join(['%s: %s' % (d, t - self.start_time) for
78 (d, t) in self.checkpoints]))
80 checkpoint = Checkpoint()
83 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
86 """Return HTML code for a (?) link to a specified help topic"""
87 return ('<span class="helplink"><a href="help?' +
88 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
89 +'" target="_blank" ' +
90 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
92 def makeErrorPre(old, addition):
96 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
98 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
100 Template.database = database
101 Template.config = config
102 Template.helppopup = staticmethod(helppopup)
106 """Class to store a dictionary that will be converted to JSON"""
107 def __init__(self, **kws):
115 return simplejson.dumps(self.data)
117 def addError(self, text):
118 """Add stderr text to be displayed on the website."""
120 makeErrorPre(self.data.get('err'), text)
123 """Class to store default values for fields."""
132 def __init__(self, max_memory=None, max_disk=None, **kws):
133 if max_memory is not None:
134 self.memory = min(self.memory, max_memory)
135 if max_disk is not None:
136 self.disk = min(self.disk, max_disk)
138 setattr(self, key, kws[key])
142 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
144 def invalidInput(op, username, fields, err, emsg):
145 """Print an error page when an InvalidInput exception occurs"""
146 d = dict(op=op, user=username, err_field=err.err_field,
147 err_value=str(err.err_value), stderr=emsg,
148 errorMessage=str(err))
149 return templates.invalid(searchList=[d])
152 """Does the machine with a given status list support VNC?"""
156 if l[0] == 'device' and l[1][0] == 'vfb':
158 return 'location' in d
161 def parseCreate(username, state, fields):
162 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
163 validate = validation.Validate(username, state, strict=True, **kws)
164 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
165 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
166 cdrom=getattr(validate, 'cdrom', None),
167 autoinstall=getattr(validate, 'autoinstall', None))
169 def create(username, state, path, fields):
170 """Handler for create requests."""
172 parsed_fields = parseCreate(username, state, fields)
173 machine = controls.createVm(username, state, **parsed_fields)
174 except InvalidInput, err:
178 state.clear() #Changed global state
179 d = getListDict(username, state)
182 for field in fields.keys():
183 setattr(d['defaults'], field, fields.getfirst(field))
185 d['new_machine'] = parsed_fields['name']
186 return templates.list(searchList=[d])
189 def getListDict(username, state):
190 """Gets the list of local variables used by list.tmpl."""
191 checkpoint.checkpoint('Starting')
192 machines = state.machines
193 checkpoint.checkpoint('Got my machines')
196 xmlist = state.xmlist
197 checkpoint.checkpoint('Got uptimes')
198 can_clone = 'ice3' not in state.xmlist_raw
204 m.uptime = xmlist[m]['uptime']
205 if xmlist[m]['console']:
210 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
211 max_memory = validation.maxMemory(username, state)
212 max_disk = validation.maxDisk(username)
213 checkpoint.checkpoint('Got max mem/disk')
214 defaults = Defaults(max_memory=max_memory,
217 checkpoint.checkpoint('Got defaults')
218 def sortkey(machine):
219 return (machine.owner != username, machine.owner, machine.name)
220 machines = sorted(machines, key=sortkey)
221 d = dict(user=username,
222 cant_add_vm=validation.cantAddVm(username, state),
223 max_memory=max_memory,
231 def listVms(username, state, path, fields):
232 """Handler for list requests."""
233 checkpoint.checkpoint('Getting list dict')
234 d = getListDict(username, state)
235 checkpoint.checkpoint('Got list dict')
236 return templates.list(searchList=[d])
238 def vnc(username, state, path, fields):
241 Note that due to same-domain restrictions, the applet connects to
242 the webserver, which needs to forward those requests to the xen
243 server. The Xen server runs another proxy that (1) authenticates
244 and (2) finds the correct port for the VM.
246 You might want iptables like:
248 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
249 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
250 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
251 --dport 10003 -j SNAT --to-source 18.187.7.142
252 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
253 --dport 10003 -j ACCEPT
255 Remember to enable iptables!
256 echo 1 > /proc/sys/net/ipv4/ip_forward
258 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
260 token = controls.vnctoken(machine)
261 host = controls.listHost(machine)
263 port = 10003 + [h.hostname for h in config.hosts].index(host)
267 status = controls.statusInfo(machine)
268 has_vnc = hasVnc(status)
270 d = dict(user=username,
274 hostname=state.environ.get('SERVER_NAME', 'localhost'),
277 return templates.vnc(searchList=[d])
279 def getHostname(nic):
280 """Find the hostname associated with a NIC.
282 XXX this should be merged with the similar logic in DNS and DHCP.
285 hostname = nic.hostname
287 hostname = nic.machine.name
293 return hostname + '.' + config.dns.domains[0]
295 def getNicInfo(data_dict, machine):
296 """Helper function for info, get data on nics for a machine.
298 Modifies data_dict to include the relevant data, and returns a list
299 of (key, name) pairs to display "name: data_dict[key]" to the user.
301 data_dict['num_nics'] = len(machine.nics)
302 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
303 ('nic%s_mac', 'NIC %s MAC Addr'),
304 ('nic%s_ip', 'NIC %s IP'),
307 for i in range(len(machine.nics)):
308 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
309 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
310 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
311 data_dict['nic%s_ip' % i] = machine.nics[i].ip
312 if len(machine.nics) == 1:
313 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
316 def getDiskInfo(data_dict, machine):
317 """Helper function for info, get data on disks for a machine.
319 Modifies data_dict to include the relevant data, and returns a list
320 of (key, name) pairs to display "name: data_dict[key]" to the user.
322 data_dict['num_disks'] = len(machine.disks)
323 disk_fields_template = [('%s_size', '%s size')]
325 for disk in machine.disks:
326 name = disk.guest_device_name
327 disk_fields.extend([(x % name, y % name) for x, y in
328 disk_fields_template])
329 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
332 def command(username, state, path, fields):
333 """Handler for running commands like boot and delete on a VM."""
334 back = fields.getfirst('back')
336 d = controls.commandResult(username, state, fields)
337 if d['command'] == 'Delete VM':
339 except InvalidInput, err:
342 print >> sys.stderr, err
347 return templates.command(searchList=[d])
349 state.clear() #Changed global state
350 d = getListDict(username, state)
352 return templates.list(searchList=[d])
354 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
355 return ({'Status': '303 See Other',
356 'Location': 'info?machine_id=%d' % machine.machine_id},
357 "You shouldn't see this message.")
359 raise InvalidInput('back', back, 'Not a known back page.')
361 def modifyDict(username, state, fields):
362 """Modify a machine as specified by CGI arguments.
364 Return a list of local variables for modify.tmpl.
369 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
370 validate = validation.Validate(username, state, **kws)
371 machine = validate.machine
372 oldname = machine.name
374 if hasattr(validate, 'memory'):
375 machine.memory = validate.memory
377 if hasattr(validate, 'vmtype'):
378 machine.type = validate.vmtype
380 if hasattr(validate, 'disksize'):
381 disksize = validate.disksize
382 disk = machine.disks[0]
383 if disk.size != disksize:
384 olddisk[disk.guest_device_name] = disksize
386 session.save_or_update(disk)
389 if hasattr(validate, 'owner') and validate.owner != machine.owner:
390 machine.owner = validate.owner
392 if hasattr(validate, 'name'):
393 machine.name = validate.name
394 for n in machine.nics:
395 if n.hostname == oldname:
396 n.hostname = validate.name
397 if hasattr(validate, 'description'):
398 machine.description = validate.description
399 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
400 machine.administrator = validate.admin
402 if hasattr(validate, 'contact'):
403 machine.contact = validate.contact
405 session.save_or_update(machine)
407 cache_acls.refreshMachine(machine)
412 for diskname in olddisk:
413 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
414 if hasattr(validate, 'name'):
415 controls.renameMachine(machine, oldname, validate.name)
416 return dict(user=username,
420 def modify(username, state, path, fields):
421 """Handler for modifying attributes of a machine."""
423 modify_dict = modifyDict(username, state, fields)
424 except InvalidInput, err:
426 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
428 machine = modify_dict['machine']
431 info_dict = infoDict(username, state, machine)
432 info_dict['err'] = err
434 for field in fields.keys():
435 setattr(info_dict['defaults'], field, fields.getfirst(field))
436 info_dict['result'] = result
437 return templates.info(searchList=[info_dict])
440 def helpHandler(username, state, path, fields):
441 """Handler for help messages."""
442 simple = fields.getfirst('simple')
443 subjects = fields.getlist('subject')
447 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
448 ParaVM. You can access the resulting system by logging into the <a
449 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
450 with your Kerberos tickets; there is no root password so sshd will
453 <p>Under the covers, the autoinstaller uses our own patched version of
454 xen-create-image, which is a tool based on debootstrap. If you log
455 into the serial console while the install is running, you can watch
458 'ParaVM Console': """
459 ParaVM machines do not support local console access over VNC. To
460 access the serial console of these machines, you can SSH with Kerberos
461 to %s, using the name of the machine as your
462 username.""" % config.console.hostname,
464 HVM machines use the virtualization features of the processor, while
465 ParaVM machines rely on a modified kernel to communicate directly with
466 the hypervisor. HVMs support boot CDs of any operating system, and
467 the VNC console applet. The three-minute autoinstaller produces
468 ParaVMs. ParaVMs typically are more efficient, and always support the
469 <a href="help?subject=ParaVM+Console">console server</a>.</p>
471 <p>More details are <a
472 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
473 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
474 (which you can skip by using the autoinstaller to begin with.)</p>
476 <p>We recommend using a ParaVM when possible and an HVM when necessary.
479 Don't ask us! We're as mystified as you are.""",
481 The owner field is used to determine <a
482 href="help?subject=Quotas">quotas</a>. It must be the name of a
483 locker that you are an AFS administrator of. In particular, you or an
484 AFS group you are a member of must have AFS rlidwka bits on the
485 locker. You can check who administers the LOCKER locker using the
486 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
487 href="help?subject=Administrator">administrator</a>.""",
489 The administrator field determines who can access the console and
490 power on and off the machine. This can be either a user or a moira
493 Quotas are determined on a per-locker basis. Each locker may have a
494 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
497 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
498 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
499 your machine will run just fine, but the applet's display of the
500 console will suffer artifacts.
503 <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 requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).<br>
504 <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.
509 subjects = sorted(help_mapping.keys())
511 d = dict(user=username,
514 mapping=help_mapping)
516 return templates.help(searchList=[d])
519 def badOperation(u, s, p, e):
520 """Function called when accessing an unknown URI."""
521 return ({'Status': '404 Not Found'}, 'Invalid operation.')
523 def infoDict(username, state, machine):
524 """Get the variables used by info.tmpl."""
525 status = controls.statusInfo(machine)
526 checkpoint.checkpoint('Getting status info')
527 has_vnc = hasVnc(status)
529 main_status = dict(name=machine.name,
530 memory=str(machine.memory))
534 main_status = dict(status[1:])
535 main_status['host'] = controls.listHost(machine)
536 start_time = float(main_status.get('start_time', 0))
537 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
538 cpu_time_float = float(main_status.get('cpu_time', 0))
539 cputime = datetime.timedelta(seconds=int(cpu_time_float))
540 checkpoint.checkpoint('Status')
541 display_fields = [('name', 'Name'),
542 ('description', 'Description'),
544 ('administrator', 'Administrator'),
545 ('contact', 'Contact'),
548 ('uptime', 'uptime'),
549 ('cputime', 'CPU usage'),
550 ('host', 'Hosted on'),
553 ('state', 'state (xen format)'),
554 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
558 machine_info['name'] = machine.name
559 machine_info['description'] = machine.description
560 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
561 machine_info['owner'] = machine.owner
562 machine_info['administrator'] = machine.administrator
563 machine_info['contact'] = machine.contact
565 nic_fields = getNicInfo(machine_info, machine)
566 nic_point = display_fields.index('NIC_INFO')
567 display_fields = (display_fields[:nic_point] + nic_fields +
568 display_fields[nic_point+1:])
570 disk_fields = getDiskInfo(machine_info, machine)
571 disk_point = display_fields.index('DISK_INFO')
572 display_fields = (display_fields[:disk_point] + disk_fields +
573 display_fields[disk_point+1:])
575 main_status['memory'] += ' MiB'
576 for field, disp in display_fields:
577 if field in ('uptime', 'cputime') and locals()[field] is not None:
578 fields.append((disp, locals()[field]))
579 elif field in machine_info:
580 fields.append((disp, machine_info[field]))
581 elif field in main_status:
582 fields.append((disp, main_status[field]))
585 #fields.append((disp, None))
587 checkpoint.checkpoint('Got fields')
590 max_mem = validation.maxMemory(machine.owner, state, machine, False)
591 checkpoint.checkpoint('Got mem')
592 max_disk = validation.maxDisk(machine.owner, machine)
593 defaults = Defaults()
594 for name in 'machine_id name description administrator owner memory contact'.split():
595 setattr(defaults, name, getattr(machine, name))
596 defaults.type = machine.type.type_id
597 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
598 checkpoint.checkpoint('Got defaults')
599 d = dict(user=username,
600 on=status is not None,
608 owner_help=helppopup("Owner"),
612 def info(username, state, path, fields):
613 """Handler for info on a single VM."""
614 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
615 d = infoDict(username, state, machine)
616 checkpoint.checkpoint('Got infodict')
617 return templates.info(searchList=[d])
619 def unauthFront(_, _2, _3, fields):
620 """Information for unauth'd users."""
621 return templates.unauth(searchList=[{'simple' : True,
622 'hostname' : socket.getfqdn()}])
624 def admin(username, state, path, fields):
626 return ({'Status': '303 See Other',
627 'Location': 'admin/'},
628 "You shouldn't see this message.")
629 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
630 raise InvalidInput('username', username,
631 'Not in admin group %s.' % config.adminacl)
632 newstate = State(username, isadmin=True)
633 newstate.environ = state.environ
634 return handler(username, newstate, path, fields)
636 def throwError(_, __, ___, ____):
637 """Throw an error, to test the error-tracing mechanisms."""
638 raise RuntimeError("test of the emergency broadcast system")
640 mapping = dict(list=listVms,
650 errortest=throwError)
652 def printHeaders(headers):
653 """Print a dictionary as HTTP headers."""
654 for key, value in headers.iteritems():
655 print '%s: %s' % (key, value)
658 def send_error_mail(subject, body):
661 to = config.web.errormail
667 """ % (to, config.web.hostname, subject, body)
668 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
669 stdin=subprocess.PIPE)
674 def show_error(op, username, fields, err, emsg, traceback):
675 """Print an error page when an exception occurs"""
676 d = dict(op=op, user=username, fields=fields,
677 errorMessage=str(err), stderr=emsg, traceback=traceback)
678 details = templates.error_raw(searchList=[d])
679 exclude = config.web.errormail_exclude
680 if username not in exclude and '*' not in exclude:
681 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
683 d['details'] = details
684 return templates.error(searchList=[d])
686 def getUser(environ):
687 """Return the current user based on the SSL environment variables"""
688 user = environ.get('REMOTE_USER')
692 if environ.get('AUTH_TYPE') == 'Negotiate':
693 # Convert the krb5 principal into a krb4 username
694 if not user.endswith('@%s' % config.kerberos.realm):
697 return user.split('@')[0].replace('/', '.')
701 def handler(username, state, path, fields):
702 operation, path = pathSplit(path)
705 print 'Starting', operation
706 fun = mapping.get(operation, badOperation)
707 return fun(username, state, path, fields)
710 def __init__(self, environ, start_response):
711 self.environ = environ
712 self.start = start_response
714 self.username = getUser(environ)
715 self.state = State(self.username)
716 self.state.environ = environ
721 start_time = time.time()
722 database.clear_cache()
723 sys.stderr = StringIO()
724 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
725 operation = self.environ.get('PATH_INFO', '')
727 self.start("301 Moved Permanently", [('Location', './')])
729 if self.username is None:
733 checkpoint.checkpoint('Before')
734 output = handler(self.username, self.state, operation, fields)
735 checkpoint.checkpoint('After')
737 headers = dict(DEFAULT_HEADERS)
738 if isinstance(output, tuple):
739 new_headers, output = output
740 headers.update(new_headers)
741 e = revertStandardError()
743 if hasattr(output, 'addError'):
746 # This only happens on redirects, so it'd be a pain to get
747 # the message to the user. Maybe in the response is useful.
748 output = output + '\n\nstderr:\n' + e
749 output_string = str(output)
750 checkpoint.checkpoint('output as a string')
751 except Exception, err:
752 if not fields.has_key('js'):
753 if isinstance(err, InvalidInput):
754 self.start('200 OK', [('Content-Type', 'text/html')])
755 e = revertStandardError()
756 yield str(invalidInput(operation, self.username, fields,
760 self.start('500 Internal Server Error',
761 [('Content-Type', 'text/html')])
762 e = revertStandardError()
763 s = show_error(operation, self.username, fields,
764 err, e, traceback.format_exc())
767 status = headers.setdefault('Status', '200 OK')
768 del headers['Status']
769 self.start(status, headers.items())
771 if fields.has_key('timedebug'):
772 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
779 from flup.server.fcgi_fork import WSGIServer
780 WSGIServer(constructor()).run()
782 if __name__ == '__main__':