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 InvirtUnauthWeb(View):
52 @cherrypy.tools.mako(filename="/unauth.mako")
54 return {'simple': True}
56 class InvirtWeb(View):
58 super(self.__class__,self).__init__()
60 self._cp_config['tools.require_login.on'] = True
61 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
62 'from invirt import database']
64 def __getattr__(self, name):
65 if name in ("admin", "overlord"):
66 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz[0].cell):
67 raise InvalidInput('username', cherrypy.request.login,
68 'Not in admin group %s.' % config.adminacl)
69 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
72 return super(InvirtWeb, self).__getattr__(name)
75 @cherrypy.tools.mako(filename="/list.mako")
76 def list(self, result=None):
77 """Handler for list requests."""
78 checkpoint.checkpoint('Getting list dict')
79 d = getListDict(cherrypy.request.login, cherrypy.request.state)
80 if result is not None:
82 checkpoint.checkpoint('Got list dict')
87 @cherrypy.tools.mako(filename="/help.mako")
88 def help(self, subject=None, simple=False):
89 """Handler for help messages."""
93 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
94 ParaVM. You can access the resulting system by logging into the <a
95 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
96 with your Kerberos tickets; there is no root password so sshd will
99 <p>Under the covers, the autoinstaller uses our own patched version of
100 xen-create-image, which is a tool based on debootstrap. If you log
101 into the serial console while the install is running, you can watch
104 'ParaVM Console': """
105 ParaVM machines do not support local console access over VNC. To
106 access the serial console of these machines, you can SSH with Kerberos
107 to %s, using the name of the machine as your
108 username.""" % config.console.hostname,
110 HVM machines use the virtualization features of the processor, while
111 ParaVM machines rely on a modified kernel to communicate directly with
112 the hypervisor. HVMs support boot CDs of any operating system, and
113 the VNC console applet. The three-minute autoinstaller produces
114 ParaVMs. ParaVMs typically are more efficient, and always support the
115 <a href="help?subject=ParaVM+Console">console server</a>.</p>
117 <p>More details are <a
118 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
119 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
120 (which you can skip by using the autoinstaller to begin with.)</p>
122 <p>We recommend using a ParaVM when possible and an HVM when necessary.
125 Don't ask us! We're as mystified as you are.""",
127 The owner field is used to determine <a
128 href="help?subject=Quotas">quotas</a>. It must be the name of a
129 locker that you are an AFS administrator of. In particular, you or an
130 AFS group you are a member of must have AFS rlidwka bits on the
131 locker. You can check who administers the LOCKER locker using the
132 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
133 href="help?subject=Administrator">administrator</a>.""",
135 The administrator field determines who can access the console and
136 power on and off the machine. This can be either a user or a moira
139 Quotas are determined on a per-locker basis. Each locker may have a
140 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
143 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
144 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
145 your machine will run just fine, but the applet's display of the
146 console will suffer artifacts.
149 <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>
150 <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, or visit <a href="http://msca.mit.edu/">http://msca.mit.edu/</a> if you are staff/faculty to request one.
155 subject = sorted(help_mapping.keys())
156 if not isinstance(subject, list):
159 return dict(simple=simple,
161 mapping=help_mapping)
162 help._cp_config['tools.require_login.on'] = False
164 def parseCreate(self, fields):
165 kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
166 validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
167 return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
168 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
169 cdrom=getattr(validate, 'cdrom', None),
170 autoinstall=getattr(validate, 'autoinstall', None))
173 @cherrypy.tools.mako(filename="/list.mako")
174 @cherrypy.tools.require_POST()
175 def create(self, **fields):
176 """Handler for create requests."""
178 parsed_fields = self.parseCreate(fields)
179 machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
180 except InvalidInput, err:
184 cherrypy.request.state.clear() #Changed global state
185 d = getListDict(cherrypy.request.login, cherrypy.request.state)
188 for field in fields.keys():
189 setattr(d['defaults'], field, fields.get(field))
191 d['new_machine'] = parsed_fields['name']
195 @cherrypy.tools.mako(filename="/helloworld.mako")
196 def helloworld(self, **kwargs):
197 return {'request': cherrypy.request, 'kwargs': kwargs}
198 helloworld._cp_config['tools.require_login.on'] = False
202 """Throw an error, to test the error-tracing mechanisms."""
203 raise RuntimeError("test of the emergency broadcast system")
205 class MachineView(View):
206 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
207 # _cp_dispatch, and parse the argument as a list instead of
210 def __getattr__(self, name):
212 machine_id = int(name)
213 cherrypy.request.params['machine_id'] = machine_id
219 @cherrypy.tools.mako(filename="/info.mako")
220 def info(self, machine_id):
221 """Handler for info on a single VM."""
222 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
223 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
224 checkpoint.checkpoint('Got infodict')
229 @cherrypy.tools.mako(filename="/info.mako")
230 @cherrypy.tools.require_POST()
231 def modify(self, machine_id, **fields):
232 """Handler for modifying attributes of a machine."""
234 modify_dict = modifyDict(cherrypy.request.login, cherrypy.request.state, machine_id, fields)
235 except InvalidInput, err:
237 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
239 machine = modify_dict['machine']
242 info_dict = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
243 info_dict['err'] = err
245 for field in fields.keys():
246 setattr(info_dict['defaults'], field, fields.get(field))
247 info_dict['result'] = result
251 @cherrypy.tools.mako(filename="/vnc.mako")
252 def vnc(self, machine_id):
255 Note that due to same-domain restrictions, the applet connects to
256 the webserver, which needs to forward those requests to the xen
257 server. The Xen server runs another proxy that (1) authenticates
258 and (2) finds the correct port for the VM.
260 You might want iptables like:
262 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
263 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
264 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
265 --dport 10003 -j SNAT --to-source 18.187.7.142
266 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
267 --dport 10003 -j ACCEPT
269 Remember to enable iptables!
270 echo 1 > /proc/sys/net/ipv4/ip_forward
272 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
274 token = controls.vnctoken(machine)
275 host = controls.listHost(machine)
277 port = 10003 + [h.hostname for h in config.hosts].index(host)
281 status = controls.statusInfo(machine)
282 has_vnc = hasVnc(status)
287 hostname=cherrypy.request.local.name,
292 @cherrypy.tools.mako(filename="/command.mako")
293 @cherrypy.tools.require_POST()
294 def command(self, command_name, machine_id, **kwargs):
295 """Handler for running commands like boot and delete on a VM."""
296 back = kwargs.get('back', None)
298 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
299 if d['command'] == 'Delete VM':
301 except InvalidInput, err:
304 print >> sys.stderr, err
311 cherrypy.request.state.clear() #Changed global state
312 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
314 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
316 raise InvalidInput('back', back, 'Not a known back page.')
318 machine = MachineView()
321 if path.startswith('/'):
326 return path[:i], path[i:]
330 self.start_time = time.time()
331 self.checkpoints = []
333 def checkpoint(self, s):
334 self.checkpoints.append((s, time.time()))
337 return ('Timing info:\n%s\n' %
338 '\n'.join(['%s: %s' % (d, t - self.start_time) for
339 (d, t) in self.checkpoints]))
341 checkpoint = Checkpoint()
343 def makeErrorPre(old, addition):
347 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
349 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
351 Template.database = database
352 Template.config = config
356 """Class to store a dictionary that will be converted to JSON"""
357 def __init__(self, **kws):
365 return simplejson.dumps(self.data)
367 def addError(self, text):
368 """Add stderr text to be displayed on the website."""
370 makeErrorPre(self.data.get('err'), text)
373 """Class to store default values for fields."""
383 def __init__(self, max_memory=None, max_disk=None, **kws):
384 if max_memory is not None:
385 self.memory = min(self.memory, max_memory)
386 if max_disk is not None:
387 self.disk = min(self.disk, max_disk)
389 setattr(self, key, kws[key])
393 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
395 def invalidInput(op, username, fields, err, emsg):
396 """Print an error page when an InvalidInput exception occurs"""
397 d = dict(op=op, user=username, err_field=err.err_field,
398 err_value=str(err.err_value), stderr=emsg,
399 errorMessage=str(err))
400 return templates.invalid(searchList=[d])
403 """Does the machine with a given status list support VNC?"""
407 if l[0] == 'device' and l[1][0] == 'vfb':
409 return 'location' in d
413 def getListDict(username, state):
414 """Gets the list of local variables used by list.tmpl."""
415 checkpoint.checkpoint('Starting')
416 machines = state.machines
417 checkpoint.checkpoint('Got my machines')
421 xmlist = state.xmlist
422 checkpoint.checkpoint('Got uptimes')
428 m.uptime = xmlist[m]['uptime']
429 if xmlist[m]['console']:
434 has_vnc[m] = "ParaVM"
435 if xmlist[m].get('autoinstall'):
438 installing[m] = False
439 max_memory = validation.maxMemory(username, state)
440 max_disk = validation.maxDisk(username)
441 checkpoint.checkpoint('Got max mem/disk')
442 defaults = Defaults(max_memory=max_memory,
445 checkpoint.checkpoint('Got defaults')
446 def sortkey(machine):
447 return (machine.owner != username, machine.owner, machine.name)
448 machines = sorted(machines, key=sortkey)
449 d = dict(user=username,
450 cant_add_vm=validation.cantAddVm(username, state),
451 max_memory=max_memory,
456 installing=installing)
459 def getHostname(nic):
460 """Find the hostname associated with a NIC.
462 XXX this should be merged with the similar logic in DNS and DHCP.
465 hostname = nic.hostname
467 hostname = nic.machine.name
473 return hostname + '.' + config.dns.domains[0]
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] = getHostname(machine.nics[i])
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
508 disk_fields_template])
509 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
512 def modifyDict(username, state, machine_id, fields):
513 """Modify a machine as specified by CGI arguments.
515 Return a dict containing the machine that was modified.
520 kws = dict([(kw, fields.get(kw)) for kw in 'owner admin contact name description memory vmtype disksize'.split() if fields.get(kw)])
521 kws['machine_id'] = machine_id
522 validate = validation.Validate(username, state, **kws)
523 machine = validate.machine
524 oldname = machine.name
526 if hasattr(validate, 'memory'):
527 machine.memory = validate.memory
529 if hasattr(validate, 'vmtype'):
530 machine.type = validate.vmtype
532 if hasattr(validate, 'disksize'):
533 disksize = validate.disksize
534 disk = machine.disks[0]
535 if disk.size != disksize:
536 olddisk[disk.guest_device_name] = disksize
538 session.save_or_update(disk)
541 if hasattr(validate, 'owner') and validate.owner != machine.owner:
542 machine.owner = validate.owner
544 if hasattr(validate, 'name'):
545 machine.name = validate.name
546 for n in machine.nics:
547 if n.hostname == oldname:
548 n.hostname = validate.name
549 if hasattr(validate, 'description'):
550 machine.description = validate.description
551 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
552 machine.administrator = validate.admin
554 if hasattr(validate, 'contact'):
555 machine.contact = validate.contact
557 session.save_or_update(machine)
559 cache_acls.refreshMachine(machine)
564 for diskname in olddisk:
565 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
566 if hasattr(validate, 'name'):
567 controls.renameMachine(machine, oldname, validate.name)
568 return dict(machine=machine)
571 def badOperation(u, s, p, e):
572 """Function called when accessing an unknown URI."""
573 return ({'Status': '404 Not Found'}, 'Invalid operation.')
575 def infoDict(username, state, machine):
576 """Get the variables used by info.tmpl."""
577 status = controls.statusInfo(machine)
578 checkpoint.checkpoint('Getting status info')
579 has_vnc = hasVnc(status)
581 main_status = dict(name=machine.name,
582 memory=str(machine.memory))
586 main_status = dict(status[1:])
587 main_status['host'] = controls.listHost(machine)
588 start_time = float(main_status.get('start_time', 0))
589 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
590 cpu_time_float = float(main_status.get('cpu_time', 0))
591 cputime = datetime.timedelta(seconds=int(cpu_time_float))
592 checkpoint.checkpoint('Status')
593 display_fields = [('name', 'Name'),
594 ('description', 'Description'),
596 ('administrator', 'Administrator'),
597 ('contact', 'Contact'),
600 ('uptime', 'uptime'),
601 ('cputime', 'CPU usage'),
602 ('host', 'Hosted on'),
605 ('state', 'state (xen format)'),
609 machine_info['name'] = machine.name
610 machine_info['description'] = machine.description
611 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
612 machine_info['owner'] = machine.owner
613 machine_info['administrator'] = machine.administrator
614 machine_info['contact'] = machine.contact
616 nic_fields = getNicInfo(machine_info, machine)
617 nic_point = display_fields.index('NIC_INFO')
618 display_fields = (display_fields[:nic_point] + nic_fields +
619 display_fields[nic_point+1:])
621 disk_fields = getDiskInfo(machine_info, machine)
622 disk_point = display_fields.index('DISK_INFO')
623 display_fields = (display_fields[:disk_point] + disk_fields +
624 display_fields[disk_point+1:])
626 main_status['memory'] += ' MiB'
627 for field, disp in display_fields:
628 if field in ('uptime', 'cputime') and locals()[field] is not None:
629 fields.append((disp, locals()[field]))
630 elif field in machine_info:
631 fields.append((disp, machine_info[field]))
632 elif field in main_status:
633 fields.append((disp, main_status[field]))
636 #fields.append((disp, None))
638 checkpoint.checkpoint('Got fields')
641 max_mem = validation.maxMemory(machine.owner, state, machine, False)
642 checkpoint.checkpoint('Got mem')
643 max_disk = validation.maxDisk(machine.owner, machine)
644 defaults = Defaults()
645 for name in 'machine_id name description administrator owner memory contact'.split():
646 if getattr(machine, name):
647 setattr(defaults, name, getattr(machine, name))
648 defaults.type = machine.type.type_id
649 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
650 checkpoint.checkpoint('Got defaults')
651 d = dict(user=username,
652 on=status is not None,
665 def printHeaders(headers):
666 """Print a dictionary as HTTP headers."""
667 for key, value in headers.iteritems():
668 print '%s: %s' % (key, value)
671 def send_error_mail(subject, body):
674 to = config.web.errormail
680 """ % (to, config.web.hostname, subject, body)
681 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
682 stdin=subprocess.PIPE)
687 def show_error(op, username, fields, err, emsg, traceback):
688 """Print an error page when an exception occurs"""
689 d = dict(op=op, user=username, fields=fields,
690 errorMessage=str(err), stderr=emsg, traceback=traceback)
691 details = templates.error_raw(searchList=[d])
692 exclude = config.web.errormail_exclude
693 if username not in exclude and '*' not in exclude:
694 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
696 d['details'] = details
697 return templates.error(searchList=[d])
699 def handler(username, state, path, fields):
700 operation, path = pathSplit(path)
703 print 'Starting', operation
704 fun = mapping.get(operation, badOperation)
705 return fun(username, state, path, fields)
708 def __init__(self, environ, start_response):
709 self.environ = environ
710 self.start = start_response
712 self.username = getUser(environ)
713 self.state = State(self.username)
714 self.state.environ = environ
719 start_time = time.time()
720 database.clear_cache()
721 sys.stderr = StringIO()
722 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
723 operation = self.environ.get('PATH_INFO', '')
725 self.start("301 Moved Permanently", [('Location', './')])
727 if self.username is None:
731 checkpoint.checkpoint('Before')
732 output = handler(self.username, self.state, operation, fields)
733 checkpoint.checkpoint('After')
735 headers = dict(DEFAULT_HEADERS)
736 if isinstance(output, tuple):
737 new_headers, output = output
738 headers.update(new_headers)
739 e = revertStandardError()
741 if hasattr(output, 'addError'):
744 # This only happens on redirects, so it'd be a pain to get
745 # the message to the user. Maybe in the response is useful.
746 output = output + '\n\nstderr:\n' + e
747 output_string = str(output)
748 checkpoint.checkpoint('output as a string')
749 except Exception, err:
750 if not fields.has_key('js'):
751 if isinstance(err, InvalidInput):
752 self.start('200 OK', [('Content-Type', 'text/html')])
753 e = revertStandardError()
754 yield str(invalidInput(operation, self.username, fields,
758 self.start('500 Internal Server Error',
759 [('Content-Type', 'text/html')])
760 e = revertStandardError()
761 s = show_error(operation, self.username, fields,
762 err, e, traceback.format_exc())
765 status = headers.setdefault('Status', '200 OK')
766 del headers['Status']
767 self.start(status, headers.items())
769 if fields.has_key('timedebug'):
770 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
777 from flup.server.fcgi_fork import WSGIServer
778 WSGIServer(constructor()).run()
780 if __name__ == '__main__':