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.
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
150 @cherrypy.tools.mako(filename="/helloworld.mako")
151 def helloworld(self, **kwargs):
152 return {'request': cherrypy.request, 'kwargs': kwargs}
153 helloworld._cp_config['tools.require_login.on'] = False
155 class MachineView(View):
156 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
157 # _cp_dispatch, and parse the argument as a list instead of
160 def __getattr__(self, name):
162 machine_id = int(name)
163 cherrypy.request.params['machine_id'] = machine_id
169 @cherrypy.tools.mako(filename="/info.mako")
170 def info(self, machine_id):
171 """Handler for info on a single VM."""
172 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
173 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
174 checkpoint.checkpoint('Got infodict')
179 @cherrypy.tools.mako(filename="/vnc.mako")
180 def vnc(self, machine_id):
183 Note that due to same-domain restrictions, the applet connects to
184 the webserver, which needs to forward those requests to the xen
185 server. The Xen server runs another proxy that (1) authenticates
186 and (2) finds the correct port for the VM.
188 You might want iptables like:
190 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
191 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
192 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
193 --dport 10003 -j SNAT --to-source 18.187.7.142
194 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
195 --dport 10003 -j ACCEPT
197 Remember to enable iptables!
198 echo 1 > /proc/sys/net/ipv4/ip_forward
200 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
202 token = controls.vnctoken(machine)
203 host = controls.listHost(machine)
205 port = 10003 + [h.hostname for h in config.hosts].index(host)
209 status = controls.statusInfo(machine)
210 has_vnc = hasVnc(status)
215 hostname=cherrypy.request.local.name,
220 @cherrypy.tools.mako(filename="/command.mako")
221 def command(self, command_name, machine_id, **kwargs):
222 """Handler for running commands like boot and delete on a VM."""
223 if cherrypy.request.method != "POST":
224 raise InvalidInput("request.method", command_name, "You must execute commands via POST")
225 back = kwargs.get('back', None)
227 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
228 if d['command'] == 'Delete VM':
230 except InvalidInput, err:
233 print >> sys.stderr, err
240 cherrypy.request.state.clear() #Changed global state
241 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
243 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
245 raise InvalidInput('back', back, 'Not a known back page.')
247 machine = MachineView()
250 if path.startswith('/'):
255 return path[:i], path[i:]
259 self.start_time = time.time()
260 self.checkpoints = []
262 def checkpoint(self, s):
263 self.checkpoints.append((s, time.time()))
266 return ('Timing info:\n%s\n' %
267 '\n'.join(['%s: %s' % (d, t - self.start_time) for
268 (d, t) in self.checkpoints]))
270 checkpoint = Checkpoint()
272 def makeErrorPre(old, addition):
276 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
278 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
280 Template.database = database
281 Template.config = config
285 """Class to store a dictionary that will be converted to JSON"""
286 def __init__(self, **kws):
294 return simplejson.dumps(self.data)
296 def addError(self, text):
297 """Add stderr text to be displayed on the website."""
299 makeErrorPre(self.data.get('err'), text)
302 """Class to store default values for fields."""
311 def __init__(self, max_memory=None, max_disk=None, **kws):
312 if max_memory is not None:
313 self.memory = min(self.memory, max_memory)
314 if max_disk is not None:
315 self.disk = min(self.disk, max_disk)
317 setattr(self, key, kws[key])
321 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
323 def invalidInput(op, username, fields, err, emsg):
324 """Print an error page when an InvalidInput exception occurs"""
325 d = dict(op=op, user=username, err_field=err.err_field,
326 err_value=str(err.err_value), stderr=emsg,
327 errorMessage=str(err))
328 return templates.invalid(searchList=[d])
331 """Does the machine with a given status list support VNC?"""
335 if l[0] == 'device' and l[1][0] == 'vfb':
337 return 'location' in d
340 def parseCreate(username, state, fields):
341 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
342 validate = validation.Validate(username, state, strict=True, **kws)
343 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
344 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
345 cdrom=getattr(validate, 'cdrom', None),
346 autoinstall=getattr(validate, 'autoinstall', None))
348 def create(username, state, path, fields):
349 """Handler for create requests."""
351 parsed_fields = parseCreate(username, state, fields)
352 machine = controls.createVm(username, state, **parsed_fields)
353 except InvalidInput, err:
357 state.clear() #Changed global state
358 d = getListDict(username, state)
361 for field in fields.keys():
362 setattr(d['defaults'], field, fields.getfirst(field))
364 d['new_machine'] = parsed_fields['name']
365 return templates.list(searchList=[d])
368 def getListDict(username, state):
369 """Gets the list of local variables used by list.tmpl."""
370 checkpoint.checkpoint('Starting')
371 machines = state.machines
372 checkpoint.checkpoint('Got my machines')
375 xmlist = state.xmlist
376 checkpoint.checkpoint('Got uptimes')
377 can_clone = 'ice3' not in state.xmlist_raw
383 m.uptime = xmlist[m]['uptime']
384 if xmlist[m]['console']:
389 has_vnc[m] = "ParaVM"
390 max_memory = validation.maxMemory(username, state)
391 max_disk = validation.maxDisk(username)
392 checkpoint.checkpoint('Got max mem/disk')
393 defaults = Defaults(max_memory=max_memory,
396 checkpoint.checkpoint('Got defaults')
397 def sortkey(machine):
398 return (machine.owner != username, machine.owner, machine.name)
399 machines = sorted(machines, key=sortkey)
400 d = dict(user=username,
401 cant_add_vm=validation.cantAddVm(username, state),
402 max_memory=max_memory,
410 def getHostname(nic):
411 """Find the hostname associated with a NIC.
413 XXX this should be merged with the similar logic in DNS and DHCP.
416 hostname = nic.hostname
418 hostname = nic.machine.name
424 return hostname + '.' + config.dns.domains[0]
426 def getNicInfo(data_dict, machine):
427 """Helper function for info, get data on nics for a machine.
429 Modifies data_dict to include the relevant data, and returns a list
430 of (key, name) pairs to display "name: data_dict[key]" to the user.
432 data_dict['num_nics'] = len(machine.nics)
433 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
434 ('nic%s_mac', 'NIC %s MAC Addr'),
435 ('nic%s_ip', 'NIC %s IP'),
438 for i in range(len(machine.nics)):
439 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
440 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
441 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
442 data_dict['nic%s_ip' % i] = machine.nics[i].ip
443 if len(machine.nics) == 1:
444 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
447 def getDiskInfo(data_dict, machine):
448 """Helper function for info, get data on disks for a machine.
450 Modifies data_dict to include the relevant data, and returns a list
451 of (key, name) pairs to display "name: data_dict[key]" to the user.
453 data_dict['num_disks'] = len(machine.disks)
454 disk_fields_template = [('%s_size', '%s size')]
456 for disk in machine.disks:
457 name = disk.guest_device_name
458 disk_fields.extend([(x % name, y % name) for x, y in
459 disk_fields_template])
460 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
463 def modifyDict(username, state, fields):
464 """Modify a machine as specified by CGI arguments.
466 Return a list of local variables for modify.tmpl.
471 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
472 validate = validation.Validate(username, state, **kws)
473 machine = validate.machine
474 oldname = machine.name
476 if hasattr(validate, 'memory'):
477 machine.memory = validate.memory
479 if hasattr(validate, 'vmtype'):
480 machine.type = validate.vmtype
482 if hasattr(validate, 'disksize'):
483 disksize = validate.disksize
484 disk = machine.disks[0]
485 if disk.size != disksize:
486 olddisk[disk.guest_device_name] = disksize
488 session.save_or_update(disk)
491 if hasattr(validate, 'owner') and validate.owner != machine.owner:
492 machine.owner = validate.owner
494 if hasattr(validate, 'name'):
495 machine.name = validate.name
496 for n in machine.nics:
497 if n.hostname == oldname:
498 n.hostname = validate.name
499 if hasattr(validate, 'description'):
500 machine.description = validate.description
501 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
502 machine.administrator = validate.admin
504 if hasattr(validate, 'contact'):
505 machine.contact = validate.contact
507 session.save_or_update(machine)
509 cache_acls.refreshMachine(machine)
514 for diskname in olddisk:
515 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
516 if hasattr(validate, 'name'):
517 controls.renameMachine(machine, oldname, validate.name)
518 return dict(user=username,
522 def modify(username, state, path, fields):
523 """Handler for modifying attributes of a machine."""
525 modify_dict = modifyDict(username, state, fields)
526 except InvalidInput, err:
528 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
530 machine = modify_dict['machine']
533 info_dict = infoDict(username, state, machine)
534 info_dict['err'] = err
536 for field in fields.keys():
537 setattr(info_dict['defaults'], field, fields.getfirst(field))
538 info_dict['result'] = result
539 return templates.info(searchList=[info_dict])
541 def badOperation(u, s, p, e):
542 """Function called when accessing an unknown URI."""
543 return ({'Status': '404 Not Found'}, 'Invalid operation.')
545 def infoDict(username, state, machine):
546 """Get the variables used by info.tmpl."""
547 status = controls.statusInfo(machine)
548 checkpoint.checkpoint('Getting status info')
549 has_vnc = hasVnc(status)
551 main_status = dict(name=machine.name,
552 memory=str(machine.memory))
556 main_status = dict(status[1:])
557 main_status['host'] = controls.listHost(machine)
558 start_time = float(main_status.get('start_time', 0))
559 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
560 cpu_time_float = float(main_status.get('cpu_time', 0))
561 cputime = datetime.timedelta(seconds=int(cpu_time_float))
562 checkpoint.checkpoint('Status')
563 display_fields = [('name', 'Name'),
564 ('description', 'Description'),
566 ('administrator', 'Administrator'),
567 ('contact', 'Contact'),
570 ('uptime', 'uptime'),
571 ('cputime', 'CPU usage'),
572 ('host', 'Hosted on'),
575 ('state', 'state (xen format)'),
579 machine_info['name'] = machine.name
580 machine_info['description'] = machine.description
581 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
582 machine_info['owner'] = machine.owner
583 machine_info['administrator'] = machine.administrator
584 machine_info['contact'] = machine.contact
586 nic_fields = getNicInfo(machine_info, machine)
587 nic_point = display_fields.index('NIC_INFO')
588 display_fields = (display_fields[:nic_point] + nic_fields +
589 display_fields[nic_point+1:])
591 disk_fields = getDiskInfo(machine_info, machine)
592 disk_point = display_fields.index('DISK_INFO')
593 display_fields = (display_fields[:disk_point] + disk_fields +
594 display_fields[disk_point+1:])
596 main_status['memory'] += ' MiB'
597 for field, disp in display_fields:
598 if field in ('uptime', 'cputime') and locals()[field] is not None:
599 fields.append((disp, locals()[field]))
600 elif field in machine_info:
601 fields.append((disp, machine_info[field]))
602 elif field in main_status:
603 fields.append((disp, main_status[field]))
606 #fields.append((disp, None))
608 checkpoint.checkpoint('Got fields')
611 max_mem = validation.maxMemory(machine.owner, state, machine, False)
612 checkpoint.checkpoint('Got mem')
613 max_disk = validation.maxDisk(machine.owner, machine)
614 defaults = Defaults()
615 for name in 'machine_id name description administrator owner memory contact'.split():
616 setattr(defaults, name, getattr(machine, name))
617 defaults.type = machine.type.type_id
618 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
619 checkpoint.checkpoint('Got defaults')
620 d = dict(user=username,
621 on=status is not None,
632 def unauthFront(_, _2, _3, fields):
633 """Information for unauth'd users."""
634 return templates.unauth(searchList=[{'simple' : True,
635 'hostname' : socket.getfqdn()}])
637 def admin(username, state, path, fields):
639 return ({'Status': '303 See Other',
640 'Location': 'admin/'},
641 "You shouldn't see this message.")
642 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
643 raise InvalidInput('username', username,
644 'Not in admin group %s.' % config.adminacl)
645 newstate = State(username, isadmin=True)
646 newstate.environ = state.environ
647 return handler(username, newstate, path, fields)
649 def throwError(_, __, ___, ____):
650 """Throw an error, to test the error-tracing mechanisms."""
651 raise RuntimeError("test of the emergency broadcast system")
659 errortest=throwError)
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__':