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__()
54 self._cp_config['tools.require_login.on'] = True
55 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
56 'from invirt import database']
60 @cherrypy.tools.mako(filename="/list.mako")
61 def list(self, result=None):
62 """Handler for list requests."""
63 checkpoint.checkpoint('Getting list dict')
64 d = getListDict(cherrypy.request.login, cherrypy.request.state)
65 if result is not None:
67 checkpoint.checkpoint('Got list dict')
72 @cherrypy.tools.mako(filename="/help.mako")
73 def help(self, subject=None, simple=False):
74 """Handler for help messages."""
78 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
79 ParaVM. You can access the resulting system by logging into the <a
80 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
81 with your Kerberos tickets; there is no root password so sshd will
84 <p>Under the covers, the autoinstaller uses our own patched version of
85 xen-create-image, which is a tool based on debootstrap. If you log
86 into the serial console while the install is running, you can watch
90 ParaVM machines do not support local console access over VNC. To
91 access the serial console of these machines, you can SSH with Kerberos
92 to %s, using the name of the machine as your
93 username.""" % config.console.hostname,
95 HVM machines use the virtualization features of the processor, while
96 ParaVM machines rely on a modified kernel to communicate directly with
97 the hypervisor. HVMs support boot CDs of any operating system, and
98 the VNC console applet. The three-minute autoinstaller produces
99 ParaVMs. ParaVMs typically are more efficient, and always support the
100 <a href="help?subject=ParaVM+Console">console server</a>.</p>
102 <p>More details are <a
103 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
104 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
105 (which you can skip by using the autoinstaller to begin with.)</p>
107 <p>We recommend using a ParaVM when possible and an HVM when necessary.
110 Don't ask us! We're as mystified as you are.""",
112 The owner field is used to determine <a
113 href="help?subject=Quotas">quotas</a>. It must be the name of a
114 locker that you are an AFS administrator of. In particular, you or an
115 AFS group you are a member of must have AFS rlidwka bits on the
116 locker. You can check who administers the LOCKER locker using the
117 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
118 href="help?subject=Administrator">administrator</a>.""",
120 The administrator field determines who can access the console and
121 power on and off the machine. This can be either a user or a moira
124 Quotas are determined on a per-locker basis. Each locker may have a
125 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
128 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
129 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
130 your machine will run just fine, but the applet's display of the
131 console will suffer artifacts.
134 <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>
135 <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.
140 subject = sorted(help_mapping.keys())
141 if not isinstance(subject, list):
144 return dict(simple=simple,
146 mapping=help_mapping)
147 help._cp_config['tools.require_login.on'] = False
149 def parseCreate(self, fields):
150 kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
151 validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
152 return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
153 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
154 cdrom=getattr(validate, 'cdrom', None),
155 autoinstall=getattr(validate, 'autoinstall', None))
158 @cherrypy.tools.mako(filename="/list.mako")
159 @cherrypy.tools.require_POST()
160 def create(self, **fields):
161 """Handler for create requests."""
163 parsed_fields = self.parseCreate(fields)
164 machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
165 except InvalidInput, err:
169 cherrypy.request.state.clear() #Changed global state
170 d = getListDict(cherrypy.request.login, cherrypy.request.state)
173 for field in fields.keys():
174 setattr(d['defaults'], field, fields.get(field))
176 d['new_machine'] = parsed_fields['name']
180 @cherrypy.tools.mako(filename="/helloworld.mako")
181 def helloworld(self, **kwargs):
182 return {'request': cherrypy.request, 'kwargs': kwargs}
183 helloworld._cp_config['tools.require_login.on'] = False
185 class MachineView(View):
186 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
187 # _cp_dispatch, and parse the argument as a list instead of
190 def __getattr__(self, name):
192 machine_id = int(name)
193 cherrypy.request.params['machine_id'] = machine_id
199 @cherrypy.tools.mako(filename="/info.mako")
200 def info(self, machine_id):
201 """Handler for info on a single VM."""
202 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
203 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
204 checkpoint.checkpoint('Got infodict')
209 @cherrypy.tools.mako(filename="/vnc.mako")
210 def vnc(self, machine_id):
213 Note that due to same-domain restrictions, the applet connects to
214 the webserver, which needs to forward those requests to the xen
215 server. The Xen server runs another proxy that (1) authenticates
216 and (2) finds the correct port for the VM.
218 You might want iptables like:
220 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
221 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
222 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
223 --dport 10003 -j SNAT --to-source 18.187.7.142
224 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
225 --dport 10003 -j ACCEPT
227 Remember to enable iptables!
228 echo 1 > /proc/sys/net/ipv4/ip_forward
230 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
232 token = controls.vnctoken(machine)
233 host = controls.listHost(machine)
235 port = 10003 + [h.hostname for h in config.hosts].index(host)
239 status = controls.statusInfo(machine)
240 has_vnc = hasVnc(status)
245 hostname=cherrypy.request.local.name,
250 @cherrypy.tools.mako(filename="/command.mako")
251 @cherrypy.tools.require_POST()
252 def command(self, command_name, machine_id, **kwargs):
253 """Handler for running commands like boot and delete on a VM."""
254 back = kwargs.get('back', None)
256 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
257 if d['command'] == 'Delete VM':
259 except InvalidInput, err:
262 print >> sys.stderr, err
269 cherrypy.request.state.clear() #Changed global state
270 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
272 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
274 raise InvalidInput('back', back, 'Not a known back page.')
276 machine = MachineView()
279 if path.startswith('/'):
284 return path[:i], path[i:]
288 self.start_time = time.time()
289 self.checkpoints = []
291 def checkpoint(self, s):
292 self.checkpoints.append((s, time.time()))
295 return ('Timing info:\n%s\n' %
296 '\n'.join(['%s: %s' % (d, t - self.start_time) for
297 (d, t) in self.checkpoints]))
299 checkpoint = Checkpoint()
301 def makeErrorPre(old, addition):
305 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
307 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
309 Template.database = database
310 Template.config = config
314 """Class to store a dictionary that will be converted to JSON"""
315 def __init__(self, **kws):
323 return simplejson.dumps(self.data)
325 def addError(self, text):
326 """Add stderr text to be displayed on the website."""
328 makeErrorPre(self.data.get('err'), text)
331 """Class to store default values for fields."""
340 def __init__(self, max_memory=None, max_disk=None, **kws):
341 if max_memory is not None:
342 self.memory = min(self.memory, max_memory)
343 if max_disk is not None:
344 self.disk = min(self.disk, max_disk)
346 setattr(self, key, kws[key])
350 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
352 def invalidInput(op, username, fields, err, emsg):
353 """Print an error page when an InvalidInput exception occurs"""
354 d = dict(op=op, user=username, err_field=err.err_field,
355 err_value=str(err.err_value), stderr=emsg,
356 errorMessage=str(err))
357 return templates.invalid(searchList=[d])
360 """Does the machine with a given status list support VNC?"""
364 if l[0] == 'device' and l[1][0] == 'vfb':
366 return 'location' in d
370 def getListDict(username, state):
371 """Gets the list of local variables used by list.tmpl."""
372 checkpoint.checkpoint('Starting')
373 machines = state.machines
374 checkpoint.checkpoint('Got my machines')
378 xmlist = state.xmlist
379 checkpoint.checkpoint('Got uptimes')
385 m.uptime = xmlist[m]['uptime']
386 if xmlist[m]['console']:
391 has_vnc[m] = "ParaVM"
392 if xmlist[m].get('autoinstall'):
395 installing[m] = False
396 max_memory = validation.maxMemory(username, state)
397 max_disk = validation.maxDisk(username)
398 checkpoint.checkpoint('Got max mem/disk')
399 defaults = Defaults(max_memory=max_memory,
402 checkpoint.checkpoint('Got defaults')
403 def sortkey(machine):
404 return (machine.owner != username, machine.owner, machine.name)
405 machines = sorted(machines, key=sortkey)
406 d = dict(user=username,
407 cant_add_vm=validation.cantAddVm(username, state),
408 max_memory=max_memory,
413 installing=installing)
416 def getHostname(nic):
417 """Find the hostname associated with a NIC.
419 XXX this should be merged with the similar logic in DNS and DHCP.
422 hostname = nic.hostname
424 hostname = nic.machine.name
430 return hostname + '.' + config.dns.domains[0]
432 def getNicInfo(data_dict, machine):
433 """Helper function for info, get data on nics for a machine.
435 Modifies data_dict to include the relevant data, and returns a list
436 of (key, name) pairs to display "name: data_dict[key]" to the user.
438 data_dict['num_nics'] = len(machine.nics)
439 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
440 ('nic%s_mac', 'NIC %s MAC Addr'),
441 ('nic%s_ip', 'NIC %s IP'),
444 for i in range(len(machine.nics)):
445 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
446 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
447 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
448 data_dict['nic%s_ip' % i] = machine.nics[i].ip
449 if len(machine.nics) == 1:
450 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
453 def getDiskInfo(data_dict, machine):
454 """Helper function for info, get data on disks for a machine.
456 Modifies data_dict to include the relevant data, and returns a list
457 of (key, name) pairs to display "name: data_dict[key]" to the user.
459 data_dict['num_disks'] = len(machine.disks)
460 disk_fields_template = [('%s_size', '%s size')]
462 for disk in machine.disks:
463 name = disk.guest_device_name
464 disk_fields.extend([(x % name, y % name) for x, y in
465 disk_fields_template])
466 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
469 def modifyDict(username, state, fields):
470 """Modify a machine as specified by CGI arguments.
472 Return a list of local variables for modify.tmpl.
477 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
478 validate = validation.Validate(username, state, **kws)
479 machine = validate.machine
480 oldname = machine.name
482 if hasattr(validate, 'memory'):
483 machine.memory = validate.memory
485 if hasattr(validate, 'vmtype'):
486 machine.type = validate.vmtype
488 if hasattr(validate, 'disksize'):
489 disksize = validate.disksize
490 disk = machine.disks[0]
491 if disk.size != disksize:
492 olddisk[disk.guest_device_name] = disksize
494 session.save_or_update(disk)
497 if hasattr(validate, 'owner') and validate.owner != machine.owner:
498 machine.owner = validate.owner
500 if hasattr(validate, 'name'):
501 machine.name = validate.name
502 for n in machine.nics:
503 if n.hostname == oldname:
504 n.hostname = validate.name
505 if hasattr(validate, 'description'):
506 machine.description = validate.description
507 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
508 machine.administrator = validate.admin
510 if hasattr(validate, 'contact'):
511 machine.contact = validate.contact
513 session.save_or_update(machine)
515 cache_acls.refreshMachine(machine)
520 for diskname in olddisk:
521 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
522 if hasattr(validate, 'name'):
523 controls.renameMachine(machine, oldname, validate.name)
524 return dict(user=username,
528 def modify(username, state, path, fields):
529 """Handler for modifying attributes of a machine."""
531 modify_dict = modifyDict(username, state, fields)
532 except InvalidInput, err:
534 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
536 machine = modify_dict['machine']
539 info_dict = infoDict(username, state, machine)
540 info_dict['err'] = err
542 for field in fields.keys():
543 setattr(info_dict['defaults'], field, fields.getfirst(field))
544 info_dict['result'] = result
545 return templates.info(searchList=[info_dict])
547 def badOperation(u, s, p, e):
548 """Function called when accessing an unknown URI."""
549 return ({'Status': '404 Not Found'}, 'Invalid operation.')
551 def infoDict(username, state, machine):
552 """Get the variables used by info.tmpl."""
553 status = controls.statusInfo(machine)
554 checkpoint.checkpoint('Getting status info')
555 has_vnc = hasVnc(status)
557 main_status = dict(name=machine.name,
558 memory=str(machine.memory))
562 main_status = dict(status[1:])
563 main_status['host'] = controls.listHost(machine)
564 start_time = float(main_status.get('start_time', 0))
565 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
566 cpu_time_float = float(main_status.get('cpu_time', 0))
567 cputime = datetime.timedelta(seconds=int(cpu_time_float))
568 checkpoint.checkpoint('Status')
569 display_fields = [('name', 'Name'),
570 ('description', 'Description'),
572 ('administrator', 'Administrator'),
573 ('contact', 'Contact'),
576 ('uptime', 'uptime'),
577 ('cputime', 'CPU usage'),
578 ('host', 'Hosted on'),
581 ('state', 'state (xen format)'),
585 machine_info['name'] = machine.name
586 machine_info['description'] = machine.description
587 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
588 machine_info['owner'] = machine.owner
589 machine_info['administrator'] = machine.administrator
590 machine_info['contact'] = machine.contact
592 nic_fields = getNicInfo(machine_info, machine)
593 nic_point = display_fields.index('NIC_INFO')
594 display_fields = (display_fields[:nic_point] + nic_fields +
595 display_fields[nic_point+1:])
597 disk_fields = getDiskInfo(machine_info, machine)
598 disk_point = display_fields.index('DISK_INFO')
599 display_fields = (display_fields[:disk_point] + disk_fields +
600 display_fields[disk_point+1:])
602 main_status['memory'] += ' MiB'
603 for field, disp in display_fields:
604 if field in ('uptime', 'cputime') and locals()[field] is not None:
605 fields.append((disp, locals()[field]))
606 elif field in machine_info:
607 fields.append((disp, machine_info[field]))
608 elif field in main_status:
609 fields.append((disp, main_status[field]))
612 #fields.append((disp, None))
614 checkpoint.checkpoint('Got fields')
617 max_mem = validation.maxMemory(machine.owner, state, machine, False)
618 checkpoint.checkpoint('Got mem')
619 max_disk = validation.maxDisk(machine.owner, machine)
620 defaults = Defaults()
621 for name in 'machine_id name description administrator owner memory contact'.split():
622 setattr(defaults, name, getattr(machine, name))
623 defaults.type = machine.type.type_id
624 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
625 checkpoint.checkpoint('Got defaults')
626 d = dict(user=username,
627 on=status is not None,
638 def unauthFront(_, _2, _3, fields):
639 """Information for unauth'd users."""
640 return templates.unauth(searchList=[{'simple' : True,
641 'hostname' : socket.getfqdn()}])
643 def admin(username, state, path, fields):
645 return ({'Status': '303 See Other',
646 'Location': 'admin/'},
647 "You shouldn't see this message.")
648 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
649 raise InvalidInput('username', username,
650 'Not in admin group %s.' % config.adminacl)
651 newstate = State(username, isadmin=True)
652 newstate.environ = state.environ
653 return handler(username, newstate, path, fields)
655 def throwError(_, __, ___, ____):
656 """Throw an error, to test the error-tracing mechanisms."""
657 raise RuntimeError("test of the emergency broadcast system")
664 errortest=throwError)
666 def printHeaders(headers):
667 """Print a dictionary as HTTP headers."""
668 for key, value in headers.iteritems():
669 print '%s: %s' % (key, value)
672 def send_error_mail(subject, body):
675 to = config.web.errormail
681 """ % (to, config.web.hostname, subject, body)
682 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
683 stdin=subprocess.PIPE)
688 def show_error(op, username, fields, err, emsg, traceback):
689 """Print an error page when an exception occurs"""
690 d = dict(op=op, user=username, fields=fields,
691 errorMessage=str(err), stderr=emsg, traceback=traceback)
692 details = templates.error_raw(searchList=[d])
693 exclude = config.web.errormail_exclude
694 if username not in exclude and '*' not in exclude:
695 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
697 d['details'] = details
698 return templates.error(searchList=[d])
700 def handler(username, state, path, fields):
701 operation, path = pathSplit(path)
704 print 'Starting', operation
705 fun = mapping.get(operation, badOperation)
706 return fun(username, state, path, fields)
709 def __init__(self, environ, start_response):
710 self.environ = environ
711 self.start = start_response
713 self.username = getUser(environ)
714 self.state = State(self.username)
715 self.state.environ = environ
720 start_time = time.time()
721 database.clear_cache()
722 sys.stderr = StringIO()
723 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
724 operation = self.environ.get('PATH_INFO', '')
726 self.start("301 Moved Permanently", [('Location', './')])
728 if self.username is None:
732 checkpoint.checkpoint('Before')
733 output = handler(self.username, self.state, operation, fields)
734 checkpoint.checkpoint('After')
736 headers = dict(DEFAULT_HEADERS)
737 if isinstance(output, tuple):
738 new_headers, output = output
739 headers.update(new_headers)
740 e = revertStandardError()
742 if hasattr(output, 'addError'):
745 # This only happens on redirects, so it'd be a pain to get
746 # the message to the user. Maybe in the response is useful.
747 output = output + '\n\nstderr:\n' + e
748 output_string = str(output)
749 checkpoint.checkpoint('output as a string')
750 except Exception, err:
751 if not fields.has_key('js'):
752 if isinstance(err, InvalidInput):
753 self.start('200 OK', [('Content-Type', 'text/html')])
754 e = revertStandardError()
755 yield str(invalidInput(operation, self.username, fields,
759 self.start('500 Internal Server Error',
760 [('Content-Type', 'text/html')])
761 e = revertStandardError()
762 s = show_error(operation, self.username, fields,
763 err, e, traceback.format_exc())
766 status = headers.setdefault('Status', '200 OK')
767 del headers['Status']
768 self.start(status, headers.items())
770 if fields.has_key('timedebug'):
771 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
778 from flup.server.fcgi_fork import WSGIServer
779 WSGIServer(constructor()).run()
781 if __name__ == '__main__':