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']
58 def __getattr__(self, name):
59 if name in ("admin", "overlord"):
60 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz[0].cell):
61 raise InvalidInput('username', cherrypy.request.login,
62 'Not in admin group %s.' % config.adminacl)
63 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
66 return super(InvirtWeb, self).__getattr__(name)
69 @cherrypy.tools.mako(filename="/list.mako")
70 def list(self, result=None):
71 """Handler for list requests."""
72 checkpoint.checkpoint('Getting list dict')
73 d = getListDict(cherrypy.request.login, cherrypy.request.state)
74 if result is not None:
76 checkpoint.checkpoint('Got list dict')
81 @cherrypy.tools.mako(filename="/help.mako")
82 def help(self, subject=None, simple=False):
83 """Handler for help messages."""
87 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
88 ParaVM. You can access the resulting system by logging into the <a
89 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
90 with your Kerberos tickets; there is no root password so sshd will
93 <p>Under the covers, the autoinstaller uses our own patched version of
94 xen-create-image, which is a tool based on debootstrap. If you log
95 into the serial console while the install is running, you can watch
99 ParaVM machines do not support local console access over VNC. To
100 access the serial console of these machines, you can SSH with Kerberos
101 to %s, using the name of the machine as your
102 username.""" % config.console.hostname,
104 HVM machines use the virtualization features of the processor, while
105 ParaVM machines rely on a modified kernel to communicate directly with
106 the hypervisor. HVMs support boot CDs of any operating system, and
107 the VNC console applet. The three-minute autoinstaller produces
108 ParaVMs. ParaVMs typically are more efficient, and always support the
109 <a href="help?subject=ParaVM+Console">console server</a>.</p>
111 <p>More details are <a
112 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
113 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
114 (which you can skip by using the autoinstaller to begin with.)</p>
116 <p>We recommend using a ParaVM when possible and an HVM when necessary.
119 Don't ask us! We're as mystified as you are.""",
121 The owner field is used to determine <a
122 href="help?subject=Quotas">quotas</a>. It must be the name of a
123 locker that you are an AFS administrator of. In particular, you or an
124 AFS group you are a member of must have AFS rlidwka bits on the
125 locker. You can check who administers the LOCKER locker using the
126 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
127 href="help?subject=Administrator">administrator</a>.""",
129 The administrator field determines who can access the console and
130 power on and off the machine. This can be either a user or a moira
133 Quotas are determined on a per-locker basis. Each locker may have a
134 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
137 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
138 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
139 your machine will run just fine, but the applet's display of the
140 console will suffer artifacts.
143 <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>
144 <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.
149 subject = sorted(help_mapping.keys())
150 if not isinstance(subject, list):
153 return dict(simple=simple,
155 mapping=help_mapping)
156 help._cp_config['tools.require_login.on'] = False
158 def parseCreate(self, fields):
159 kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
160 validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
161 return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
162 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
163 cdrom=getattr(validate, 'cdrom', None),
164 autoinstall=getattr(validate, 'autoinstall', None))
167 @cherrypy.tools.mako(filename="/list.mako")
168 @cherrypy.tools.require_POST()
169 def create(self, **fields):
170 """Handler for create requests."""
172 parsed_fields = self.parseCreate(fields)
173 machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
174 except InvalidInput, err:
178 cherrypy.request.state.clear() #Changed global state
179 d = getListDict(cherrypy.request.login, cherrypy.request.state)
182 for field in fields.keys():
183 setattr(d['defaults'], field, fields.get(field))
185 d['new_machine'] = parsed_fields['name']
189 @cherrypy.tools.mako(filename="/helloworld.mako")
190 def helloworld(self, **kwargs):
191 return {'request': cherrypy.request, 'kwargs': kwargs}
192 helloworld._cp_config['tools.require_login.on'] = False
196 """Throw an error, to test the error-tracing mechanisms."""
197 raise RuntimeError("test of the emergency broadcast system")
199 class MachineView(View):
200 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
201 # _cp_dispatch, and parse the argument as a list instead of
204 def __getattr__(self, name):
206 machine_id = int(name)
207 cherrypy.request.params['machine_id'] = machine_id
213 @cherrypy.tools.mako(filename="/info.mako")
214 def info(self, machine_id):
215 """Handler for info on a single VM."""
216 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
217 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
218 checkpoint.checkpoint('Got infodict')
223 @cherrypy.tools.mako(filename="/vnc.mako")
224 def vnc(self, machine_id):
227 Note that due to same-domain restrictions, the applet connects to
228 the webserver, which needs to forward those requests to the xen
229 server. The Xen server runs another proxy that (1) authenticates
230 and (2) finds the correct port for the VM.
232 You might want iptables like:
234 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
235 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
236 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
237 --dport 10003 -j SNAT --to-source 18.187.7.142
238 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
239 --dport 10003 -j ACCEPT
241 Remember to enable iptables!
242 echo 1 > /proc/sys/net/ipv4/ip_forward
244 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
246 token = controls.vnctoken(machine)
247 host = controls.listHost(machine)
249 port = 10003 + [h.hostname for h in config.hosts].index(host)
253 status = controls.statusInfo(machine)
254 has_vnc = hasVnc(status)
259 hostname=cherrypy.request.local.name,
264 @cherrypy.tools.mako(filename="/command.mako")
265 @cherrypy.tools.require_POST()
266 def command(self, command_name, machine_id, **kwargs):
267 """Handler for running commands like boot and delete on a VM."""
268 back = kwargs.get('back', None)
270 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
271 if d['command'] == 'Delete VM':
273 except InvalidInput, err:
276 print >> sys.stderr, err
283 cherrypy.request.state.clear() #Changed global state
284 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
286 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
288 raise InvalidInput('back', back, 'Not a known back page.')
290 machine = MachineView()
293 if path.startswith('/'):
298 return path[:i], path[i:]
302 self.start_time = time.time()
303 self.checkpoints = []
305 def checkpoint(self, s):
306 self.checkpoints.append((s, time.time()))
309 return ('Timing info:\n%s\n' %
310 '\n'.join(['%s: %s' % (d, t - self.start_time) for
311 (d, t) in self.checkpoints]))
313 checkpoint = Checkpoint()
315 def makeErrorPre(old, addition):
319 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
321 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
323 Template.database = database
324 Template.config = config
328 """Class to store a dictionary that will be converted to JSON"""
329 def __init__(self, **kws):
337 return simplejson.dumps(self.data)
339 def addError(self, text):
340 """Add stderr text to be displayed on the website."""
342 makeErrorPre(self.data.get('err'), text)
345 """Class to store default values for fields."""
354 def __init__(self, max_memory=None, max_disk=None, **kws):
355 if max_memory is not None:
356 self.memory = min(self.memory, max_memory)
357 if max_disk is not None:
358 self.disk = min(self.disk, max_disk)
360 setattr(self, key, kws[key])
364 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
366 def invalidInput(op, username, fields, err, emsg):
367 """Print an error page when an InvalidInput exception occurs"""
368 d = dict(op=op, user=username, err_field=err.err_field,
369 err_value=str(err.err_value), stderr=emsg,
370 errorMessage=str(err))
371 return templates.invalid(searchList=[d])
374 """Does the machine with a given status list support VNC?"""
378 if l[0] == 'device' and l[1][0] == 'vfb':
380 return 'location' in d
384 def getListDict(username, state):
385 """Gets the list of local variables used by list.tmpl."""
386 checkpoint.checkpoint('Starting')
387 machines = state.machines
388 checkpoint.checkpoint('Got my machines')
392 xmlist = state.xmlist
393 checkpoint.checkpoint('Got uptimes')
399 m.uptime = xmlist[m]['uptime']
400 if xmlist[m]['console']:
405 has_vnc[m] = "ParaVM"
406 if xmlist[m].get('autoinstall'):
409 installing[m] = False
410 max_memory = validation.maxMemory(username, state)
411 max_disk = validation.maxDisk(username)
412 checkpoint.checkpoint('Got max mem/disk')
413 defaults = Defaults(max_memory=max_memory,
416 checkpoint.checkpoint('Got defaults')
417 def sortkey(machine):
418 return (machine.owner != username, machine.owner, machine.name)
419 machines = sorted(machines, key=sortkey)
420 d = dict(user=username,
421 cant_add_vm=validation.cantAddVm(username, state),
422 max_memory=max_memory,
427 installing=installing)
430 def getHostname(nic):
431 """Find the hostname associated with a NIC.
433 XXX this should be merged with the similar logic in DNS and DHCP.
436 hostname = nic.hostname
438 hostname = nic.machine.name
444 return hostname + '.' + config.dns.domains[0]
446 def getNicInfo(data_dict, machine):
447 """Helper function for info, get data on nics for a machine.
449 Modifies data_dict to include the relevant data, and returns a list
450 of (key, name) pairs to display "name: data_dict[key]" to the user.
452 data_dict['num_nics'] = len(machine.nics)
453 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
454 ('nic%s_mac', 'NIC %s MAC Addr'),
455 ('nic%s_ip', 'NIC %s IP'),
458 for i in range(len(machine.nics)):
459 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
460 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
461 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
462 data_dict['nic%s_ip' % i] = machine.nics[i].ip
463 if len(machine.nics) == 1:
464 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
467 def getDiskInfo(data_dict, machine):
468 """Helper function for info, get data on disks for a machine.
470 Modifies data_dict to include the relevant data, and returns a list
471 of (key, name) pairs to display "name: data_dict[key]" to the user.
473 data_dict['num_disks'] = len(machine.disks)
474 disk_fields_template = [('%s_size', '%s size')]
476 for disk in machine.disks:
477 name = disk.guest_device_name
478 disk_fields.extend([(x % name, y % name) for x, y in
479 disk_fields_template])
480 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
483 def modifyDict(username, state, fields):
484 """Modify a machine as specified by CGI arguments.
486 Return a list of local variables for modify.tmpl.
491 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
492 validate = validation.Validate(username, state, **kws)
493 machine = validate.machine
494 oldname = machine.name
496 if hasattr(validate, 'memory'):
497 machine.memory = validate.memory
499 if hasattr(validate, 'vmtype'):
500 machine.type = validate.vmtype
502 if hasattr(validate, 'disksize'):
503 disksize = validate.disksize
504 disk = machine.disks[0]
505 if disk.size != disksize:
506 olddisk[disk.guest_device_name] = disksize
508 session.save_or_update(disk)
511 if hasattr(validate, 'owner') and validate.owner != machine.owner:
512 machine.owner = validate.owner
514 if hasattr(validate, 'name'):
515 machine.name = validate.name
516 for n in machine.nics:
517 if n.hostname == oldname:
518 n.hostname = validate.name
519 if hasattr(validate, 'description'):
520 machine.description = validate.description
521 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
522 machine.administrator = validate.admin
524 if hasattr(validate, 'contact'):
525 machine.contact = validate.contact
527 session.save_or_update(machine)
529 cache_acls.refreshMachine(machine)
534 for diskname in olddisk:
535 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
536 if hasattr(validate, 'name'):
537 controls.renameMachine(machine, oldname, validate.name)
538 return dict(user=username,
542 def modify(username, state, path, fields):
543 """Handler for modifying attributes of a machine."""
545 modify_dict = modifyDict(username, state, fields)
546 except InvalidInput, err:
548 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
550 machine = modify_dict['machine']
553 info_dict = infoDict(username, state, machine)
554 info_dict['err'] = err
556 for field in fields.keys():
557 setattr(info_dict['defaults'], field, fields.getfirst(field))
558 info_dict['result'] = result
559 return templates.info(searchList=[info_dict])
561 def badOperation(u, s, p, e):
562 """Function called when accessing an unknown URI."""
563 return ({'Status': '404 Not Found'}, 'Invalid operation.')
565 def infoDict(username, state, machine):
566 """Get the variables used by info.tmpl."""
567 status = controls.statusInfo(machine)
568 checkpoint.checkpoint('Getting status info')
569 has_vnc = hasVnc(status)
571 main_status = dict(name=machine.name,
572 memory=str(machine.memory))
576 main_status = dict(status[1:])
577 main_status['host'] = controls.listHost(machine)
578 start_time = float(main_status.get('start_time', 0))
579 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
580 cpu_time_float = float(main_status.get('cpu_time', 0))
581 cputime = datetime.timedelta(seconds=int(cpu_time_float))
582 checkpoint.checkpoint('Status')
583 display_fields = [('name', 'Name'),
584 ('description', 'Description'),
586 ('administrator', 'Administrator'),
587 ('contact', 'Contact'),
590 ('uptime', 'uptime'),
591 ('cputime', 'CPU usage'),
592 ('host', 'Hosted on'),
595 ('state', 'state (xen format)'),
599 machine_info['name'] = machine.name
600 machine_info['description'] = machine.description
601 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
602 machine_info['owner'] = machine.owner
603 machine_info['administrator'] = machine.administrator
604 machine_info['contact'] = machine.contact
606 nic_fields = getNicInfo(machine_info, machine)
607 nic_point = display_fields.index('NIC_INFO')
608 display_fields = (display_fields[:nic_point] + nic_fields +
609 display_fields[nic_point+1:])
611 disk_fields = getDiskInfo(machine_info, machine)
612 disk_point = display_fields.index('DISK_INFO')
613 display_fields = (display_fields[:disk_point] + disk_fields +
614 display_fields[disk_point+1:])
616 main_status['memory'] += ' MiB'
617 for field, disp in display_fields:
618 if field in ('uptime', 'cputime') and locals()[field] is not None:
619 fields.append((disp, locals()[field]))
620 elif field in machine_info:
621 fields.append((disp, machine_info[field]))
622 elif field in main_status:
623 fields.append((disp, main_status[field]))
626 #fields.append((disp, None))
628 checkpoint.checkpoint('Got fields')
631 max_mem = validation.maxMemory(machine.owner, state, machine, False)
632 checkpoint.checkpoint('Got mem')
633 max_disk = validation.maxDisk(machine.owner, machine)
634 defaults = Defaults()
635 for name in 'machine_id name description administrator owner memory contact'.split():
636 setattr(defaults, name, getattr(machine, name))
637 defaults.type = machine.type.type_id
638 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
639 checkpoint.checkpoint('Got defaults')
640 d = dict(user=username,
641 on=status is not None,
652 def unauthFront(_, _2, _3, fields):
653 """Information for unauth'd users."""
654 return templates.unauth(searchList=[{'simple' : True,
655 'hostname' : socket.getfqdn()}])
661 def printHeaders(headers):
662 """Print a dictionary as HTTP headers."""
663 for key, value in headers.iteritems():
664 print '%s: %s' % (key, value)
667 def send_error_mail(subject, body):
670 to = config.web.errormail
676 """ % (to, config.web.hostname, subject, body)
677 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
678 stdin=subprocess.PIPE)
683 def show_error(op, username, fields, err, emsg, traceback):
684 """Print an error page when an exception occurs"""
685 d = dict(op=op, user=username, fields=fields,
686 errorMessage=str(err), stderr=emsg, traceback=traceback)
687 details = templates.error_raw(searchList=[d])
688 exclude = config.web.errormail_exclude
689 if username not in exclude and '*' not in exclude:
690 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
692 d['details'] = details
693 return templates.error(searchList=[d])
695 def handler(username, state, path, fields):
696 operation, path = pathSplit(path)
699 print 'Starting', operation
700 fun = mapping.get(operation, badOperation)
701 return fun(username, state, path, fields)
704 def __init__(self, environ, start_response):
705 self.environ = environ
706 self.start = start_response
708 self.username = getUser(environ)
709 self.state = State(self.username)
710 self.state.environ = environ
715 start_time = time.time()
716 database.clear_cache()
717 sys.stderr = StringIO()
718 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
719 operation = self.environ.get('PATH_INFO', '')
721 self.start("301 Moved Permanently", [('Location', './')])
723 if self.username is None:
727 checkpoint.checkpoint('Before')
728 output = handler(self.username, self.state, operation, fields)
729 checkpoint.checkpoint('After')
731 headers = dict(DEFAULT_HEADERS)
732 if isinstance(output, tuple):
733 new_headers, output = output
734 headers.update(new_headers)
735 e = revertStandardError()
737 if hasattr(output, 'addError'):
740 # This only happens on redirects, so it'd be a pain to get
741 # the message to the user. Maybe in the response is useful.
742 output = output + '\n\nstderr:\n' + e
743 output_string = str(output)
744 checkpoint.checkpoint('output as a string')
745 except Exception, err:
746 if not fields.has_key('js'):
747 if isinstance(err, InvalidInput):
748 self.start('200 OK', [('Content-Type', 'text/html')])
749 e = revertStandardError()
750 yield str(invalidInput(operation, self.username, fields,
754 self.start('500 Internal Server Error',
755 [('Content-Type', 'text/html')])
756 e = revertStandardError()
757 s = show_error(operation, self.username, fields,
758 err, e, traceback.format_exc())
761 status = headers.setdefault('Status', '200 OK')
762 del headers['Status']
763 self.start(status, headers.items())
765 if fields.has_key('timedebug'):
766 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
773 from flup.server.fcgi_fork import WSGIServer
774 WSGIServer(constructor()).run()
776 if __name__ == '__main__':