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
187 """Throw an error, to test the error-tracing mechanisms."""
188 raise RuntimeError("test of the emergency broadcast system")
190 class MachineView(View):
191 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
192 # _cp_dispatch, and parse the argument as a list instead of
195 def __getattr__(self, name):
197 machine_id = int(name)
198 cherrypy.request.params['machine_id'] = machine_id
204 @cherrypy.tools.mako(filename="/info.mako")
205 def info(self, machine_id):
206 """Handler for info on a single VM."""
207 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
208 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
209 checkpoint.checkpoint('Got infodict')
214 @cherrypy.tools.mako(filename="/vnc.mako")
215 def vnc(self, machine_id):
218 Note that due to same-domain restrictions, the applet connects to
219 the webserver, which needs to forward those requests to the xen
220 server. The Xen server runs another proxy that (1) authenticates
221 and (2) finds the correct port for the VM.
223 You might want iptables like:
225 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
226 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
227 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
228 --dport 10003 -j SNAT --to-source 18.187.7.142
229 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
230 --dport 10003 -j ACCEPT
232 Remember to enable iptables!
233 echo 1 > /proc/sys/net/ipv4/ip_forward
235 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
237 token = controls.vnctoken(machine)
238 host = controls.listHost(machine)
240 port = 10003 + [h.hostname for h in config.hosts].index(host)
244 status = controls.statusInfo(machine)
245 has_vnc = hasVnc(status)
250 hostname=cherrypy.request.local.name,
255 @cherrypy.tools.mako(filename="/command.mako")
256 @cherrypy.tools.require_POST()
257 def command(self, command_name, machine_id, **kwargs):
258 """Handler for running commands like boot and delete on a VM."""
259 back = kwargs.get('back', None)
261 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
262 if d['command'] == 'Delete VM':
264 except InvalidInput, err:
267 print >> sys.stderr, err
274 cherrypy.request.state.clear() #Changed global state
275 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
277 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
279 raise InvalidInput('back', back, 'Not a known back page.')
281 machine = MachineView()
284 if path.startswith('/'):
289 return path[:i], path[i:]
293 self.start_time = time.time()
294 self.checkpoints = []
296 def checkpoint(self, s):
297 self.checkpoints.append((s, time.time()))
300 return ('Timing info:\n%s\n' %
301 '\n'.join(['%s: %s' % (d, t - self.start_time) for
302 (d, t) in self.checkpoints]))
304 checkpoint = Checkpoint()
306 def makeErrorPre(old, addition):
310 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
312 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
314 Template.database = database
315 Template.config = config
319 """Class to store a dictionary that will be converted to JSON"""
320 def __init__(self, **kws):
328 return simplejson.dumps(self.data)
330 def addError(self, text):
331 """Add stderr text to be displayed on the website."""
333 makeErrorPre(self.data.get('err'), text)
336 """Class to store default values for fields."""
345 def __init__(self, max_memory=None, max_disk=None, **kws):
346 if max_memory is not None:
347 self.memory = min(self.memory, max_memory)
348 if max_disk is not None:
349 self.disk = min(self.disk, max_disk)
351 setattr(self, key, kws[key])
355 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
357 def invalidInput(op, username, fields, err, emsg):
358 """Print an error page when an InvalidInput exception occurs"""
359 d = dict(op=op, user=username, err_field=err.err_field,
360 err_value=str(err.err_value), stderr=emsg,
361 errorMessage=str(err))
362 return templates.invalid(searchList=[d])
365 """Does the machine with a given status list support VNC?"""
369 if l[0] == 'device' and l[1][0] == 'vfb':
371 return 'location' in d
375 def getListDict(username, state):
376 """Gets the list of local variables used by list.tmpl."""
377 checkpoint.checkpoint('Starting')
378 machines = state.machines
379 checkpoint.checkpoint('Got my machines')
383 xmlist = state.xmlist
384 checkpoint.checkpoint('Got uptimes')
390 m.uptime = xmlist[m]['uptime']
391 if xmlist[m]['console']:
396 has_vnc[m] = "ParaVM"
397 if xmlist[m].get('autoinstall'):
400 installing[m] = False
401 max_memory = validation.maxMemory(username, state)
402 max_disk = validation.maxDisk(username)
403 checkpoint.checkpoint('Got max mem/disk')
404 defaults = Defaults(max_memory=max_memory,
407 checkpoint.checkpoint('Got defaults')
408 def sortkey(machine):
409 return (machine.owner != username, machine.owner, machine.name)
410 machines = sorted(machines, key=sortkey)
411 d = dict(user=username,
412 cant_add_vm=validation.cantAddVm(username, state),
413 max_memory=max_memory,
418 installing=installing)
421 def getHostname(nic):
422 """Find the hostname associated with a NIC.
424 XXX this should be merged with the similar logic in DNS and DHCP.
427 hostname = nic.hostname
429 hostname = nic.machine.name
435 return hostname + '.' + config.dns.domains[0]
437 def getNicInfo(data_dict, machine):
438 """Helper function for info, get data on nics for a machine.
440 Modifies data_dict to include the relevant data, and returns a list
441 of (key, name) pairs to display "name: data_dict[key]" to the user.
443 data_dict['num_nics'] = len(machine.nics)
444 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
445 ('nic%s_mac', 'NIC %s MAC Addr'),
446 ('nic%s_ip', 'NIC %s IP'),
449 for i in range(len(machine.nics)):
450 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
451 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
452 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
453 data_dict['nic%s_ip' % i] = machine.nics[i].ip
454 if len(machine.nics) == 1:
455 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
458 def getDiskInfo(data_dict, machine):
459 """Helper function for info, get data on disks for a machine.
461 Modifies data_dict to include the relevant data, and returns a list
462 of (key, name) pairs to display "name: data_dict[key]" to the user.
464 data_dict['num_disks'] = len(machine.disks)
465 disk_fields_template = [('%s_size', '%s size')]
467 for disk in machine.disks:
468 name = disk.guest_device_name
469 disk_fields.extend([(x % name, y % name) for x, y in
470 disk_fields_template])
471 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
474 def modifyDict(username, state, fields):
475 """Modify a machine as specified by CGI arguments.
477 Return a list of local variables for modify.tmpl.
482 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
483 validate = validation.Validate(username, state, **kws)
484 machine = validate.machine
485 oldname = machine.name
487 if hasattr(validate, 'memory'):
488 machine.memory = validate.memory
490 if hasattr(validate, 'vmtype'):
491 machine.type = validate.vmtype
493 if hasattr(validate, 'disksize'):
494 disksize = validate.disksize
495 disk = machine.disks[0]
496 if disk.size != disksize:
497 olddisk[disk.guest_device_name] = disksize
499 session.save_or_update(disk)
502 if hasattr(validate, 'owner') and validate.owner != machine.owner:
503 machine.owner = validate.owner
505 if hasattr(validate, 'name'):
506 machine.name = validate.name
507 for n in machine.nics:
508 if n.hostname == oldname:
509 n.hostname = validate.name
510 if hasattr(validate, 'description'):
511 machine.description = validate.description
512 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
513 machine.administrator = validate.admin
515 if hasattr(validate, 'contact'):
516 machine.contact = validate.contact
518 session.save_or_update(machine)
520 cache_acls.refreshMachine(machine)
525 for diskname in olddisk:
526 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
527 if hasattr(validate, 'name'):
528 controls.renameMachine(machine, oldname, validate.name)
529 return dict(user=username,
533 def modify(username, state, path, fields):
534 """Handler for modifying attributes of a machine."""
536 modify_dict = modifyDict(username, state, fields)
537 except InvalidInput, err:
539 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
541 machine = modify_dict['machine']
544 info_dict = infoDict(username, state, machine)
545 info_dict['err'] = err
547 for field in fields.keys():
548 setattr(info_dict['defaults'], field, fields.getfirst(field))
549 info_dict['result'] = result
550 return templates.info(searchList=[info_dict])
552 def badOperation(u, s, p, e):
553 """Function called when accessing an unknown URI."""
554 return ({'Status': '404 Not Found'}, 'Invalid operation.')
556 def infoDict(username, state, machine):
557 """Get the variables used by info.tmpl."""
558 status = controls.statusInfo(machine)
559 checkpoint.checkpoint('Getting status info')
560 has_vnc = hasVnc(status)
562 main_status = dict(name=machine.name,
563 memory=str(machine.memory))
567 main_status = dict(status[1:])
568 main_status['host'] = controls.listHost(machine)
569 start_time = float(main_status.get('start_time', 0))
570 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
571 cpu_time_float = float(main_status.get('cpu_time', 0))
572 cputime = datetime.timedelta(seconds=int(cpu_time_float))
573 checkpoint.checkpoint('Status')
574 display_fields = [('name', 'Name'),
575 ('description', 'Description'),
577 ('administrator', 'Administrator'),
578 ('contact', 'Contact'),
581 ('uptime', 'uptime'),
582 ('cputime', 'CPU usage'),
583 ('host', 'Hosted on'),
586 ('state', 'state (xen format)'),
590 machine_info['name'] = machine.name
591 machine_info['description'] = machine.description
592 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
593 machine_info['owner'] = machine.owner
594 machine_info['administrator'] = machine.administrator
595 machine_info['contact'] = machine.contact
597 nic_fields = getNicInfo(machine_info, machine)
598 nic_point = display_fields.index('NIC_INFO')
599 display_fields = (display_fields[:nic_point] + nic_fields +
600 display_fields[nic_point+1:])
602 disk_fields = getDiskInfo(machine_info, machine)
603 disk_point = display_fields.index('DISK_INFO')
604 display_fields = (display_fields[:disk_point] + disk_fields +
605 display_fields[disk_point+1:])
607 main_status['memory'] += ' MiB'
608 for field, disp in display_fields:
609 if field in ('uptime', 'cputime') and locals()[field] is not None:
610 fields.append((disp, locals()[field]))
611 elif field in machine_info:
612 fields.append((disp, machine_info[field]))
613 elif field in main_status:
614 fields.append((disp, main_status[field]))
617 #fields.append((disp, None))
619 checkpoint.checkpoint('Got fields')
622 max_mem = validation.maxMemory(machine.owner, state, machine, False)
623 checkpoint.checkpoint('Got mem')
624 max_disk = validation.maxDisk(machine.owner, machine)
625 defaults = Defaults()
626 for name in 'machine_id name description administrator owner memory contact'.split():
627 setattr(defaults, name, getattr(machine, name))
628 defaults.type = machine.type.type_id
629 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
630 checkpoint.checkpoint('Got defaults')
631 d = dict(user=username,
632 on=status is not None,
643 def unauthFront(_, _2, _3, fields):
644 """Information for unauth'd users."""
645 return templates.unauth(searchList=[{'simple' : True,
646 'hostname' : socket.getfqdn()}])
648 def admin(username, state, path, fields):
650 return ({'Status': '303 See Other',
651 'Location': 'admin/'},
652 "You shouldn't see this message.")
653 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
654 raise InvalidInput('username', username,
655 'Not in admin group %s.' % config.adminacl)
656 newstate = State(username, isadmin=True)
657 newstate.environ = state.environ
658 return handler(username, newstate, path, fields)
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__':