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")
62 """Handler for list requests."""
63 checkpoint.checkpoint('Getting list dict')
64 d = getListDict(cherrypy.request.login, cherrypy.request.state)
65 checkpoint.checkpoint('Got list dict')
70 @cherrypy.tools.mako(filename="/helloworld.mako")
73 return "Hello world!\nYour request: "+repr(dir(cherrypy.request))
74 helloworld._cp_config['tools.require_login.on'] = False
77 if path.startswith('/'):
82 return path[:i], path[i:]
86 self.start_time = time.time()
89 def checkpoint(self, s):
90 self.checkpoints.append((s, time.time()))
93 return ('Timing info:\n%s\n' %
94 '\n'.join(['%s: %s' % (d, t - self.start_time) for
95 (d, t) in self.checkpoints]))
97 checkpoint = Checkpoint()
99 def makeErrorPre(old, addition):
103 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
105 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
107 Template.database = database
108 Template.config = config
112 """Class to store a dictionary that will be converted to JSON"""
113 def __init__(self, **kws):
121 return simplejson.dumps(self.data)
123 def addError(self, text):
124 """Add stderr text to be displayed on the website."""
126 makeErrorPre(self.data.get('err'), text)
129 """Class to store default values for fields."""
138 def __init__(self, max_memory=None, max_disk=None, **kws):
139 if max_memory is not None:
140 self.memory = min(self.memory, max_memory)
141 if max_disk is not None:
142 self.disk = min(self.disk, max_disk)
144 setattr(self, key, kws[key])
148 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
150 def invalidInput(op, username, fields, err, emsg):
151 """Print an error page when an InvalidInput exception occurs"""
152 d = dict(op=op, user=username, err_field=err.err_field,
153 err_value=str(err.err_value), stderr=emsg,
154 errorMessage=str(err))
155 return templates.invalid(searchList=[d])
158 """Does the machine with a given status list support VNC?"""
162 if l[0] == 'device' and l[1][0] == 'vfb':
164 return 'location' in d
167 def parseCreate(username, state, fields):
168 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
169 validate = validation.Validate(username, state, strict=True, **kws)
170 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
171 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
172 cdrom=getattr(validate, 'cdrom', None),
173 autoinstall=getattr(validate, 'autoinstall', None))
175 def create(username, state, path, fields):
176 """Handler for create requests."""
178 parsed_fields = parseCreate(username, state, fields)
179 machine = controls.createVm(username, state, **parsed_fields)
180 except InvalidInput, err:
184 state.clear() #Changed global state
185 d = getListDict(username, state)
188 for field in fields.keys():
189 setattr(d['defaults'], field, fields.getfirst(field))
191 d['new_machine'] = parsed_fields['name']
192 return templates.list(searchList=[d])
195 def getListDict(username, state):
196 """Gets the list of local variables used by list.tmpl."""
197 checkpoint.checkpoint('Starting')
198 machines = state.machines
199 checkpoint.checkpoint('Got my machines')
202 xmlist = state.xmlist
203 checkpoint.checkpoint('Got uptimes')
204 can_clone = 'ice3' not in state.xmlist_raw
210 m.uptime = xmlist[m]['uptime']
211 if xmlist[m]['console']:
216 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
217 max_memory = validation.maxMemory(username, state)
218 max_disk = validation.maxDisk(username)
219 checkpoint.checkpoint('Got max mem/disk')
220 defaults = Defaults(max_memory=max_memory,
223 checkpoint.checkpoint('Got defaults')
224 def sortkey(machine):
225 return (machine.owner != username, machine.owner, machine.name)
226 machines = sorted(machines, key=sortkey)
227 d = dict(user=username,
228 cant_add_vm=validation.cantAddVm(username, state),
229 max_memory=max_memory,
237 def vnc(username, state, path, fields):
240 Note that due to same-domain restrictions, the applet connects to
241 the webserver, which needs to forward those requests to the xen
242 server. The Xen server runs another proxy that (1) authenticates
243 and (2) finds the correct port for the VM.
245 You might want iptables like:
247 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
248 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
249 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
250 --dport 10003 -j SNAT --to-source 18.187.7.142
251 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
252 --dport 10003 -j ACCEPT
254 Remember to enable iptables!
255 echo 1 > /proc/sys/net/ipv4/ip_forward
257 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
259 token = controls.vnctoken(machine)
260 host = controls.listHost(machine)
262 port = 10003 + [h.hostname for h in config.hosts].index(host)
266 status = controls.statusInfo(machine)
267 has_vnc = hasVnc(status)
269 d = dict(user=username,
273 hostname=state.environ.get('SERVER_NAME', 'localhost'),
276 return templates.vnc(searchList=[d])
278 def getHostname(nic):
279 """Find the hostname associated with a NIC.
281 XXX this should be merged with the similar logic in DNS and DHCP.
284 hostname = nic.hostname
286 hostname = nic.machine.name
292 return hostname + '.' + config.dns.domains[0]
294 def getNicInfo(data_dict, machine):
295 """Helper function for info, get data on nics for a machine.
297 Modifies data_dict to include the relevant data, and returns a list
298 of (key, name) pairs to display "name: data_dict[key]" to the user.
300 data_dict['num_nics'] = len(machine.nics)
301 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
302 ('nic%s_mac', 'NIC %s MAC Addr'),
303 ('nic%s_ip', 'NIC %s IP'),
306 for i in range(len(machine.nics)):
307 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
308 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
309 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
310 data_dict['nic%s_ip' % i] = machine.nics[i].ip
311 if len(machine.nics) == 1:
312 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
315 def getDiskInfo(data_dict, machine):
316 """Helper function for info, get data on disks for a machine.
318 Modifies data_dict to include the relevant data, and returns a list
319 of (key, name) pairs to display "name: data_dict[key]" to the user.
321 data_dict['num_disks'] = len(machine.disks)
322 disk_fields_template = [('%s_size', '%s size')]
324 for disk in machine.disks:
325 name = disk.guest_device_name
326 disk_fields.extend([(x % name, y % name) for x, y in
327 disk_fields_template])
328 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
331 def command(username, state, path, fields):
332 """Handler for running commands like boot and delete on a VM."""
333 back = fields.getfirst('back')
335 d = controls.commandResult(username, state, fields)
336 if d['command'] == 'Delete VM':
338 except InvalidInput, err:
341 print >> sys.stderr, err
346 return templates.command(searchList=[d])
348 state.clear() #Changed global state
349 d = getListDict(username, state)
351 return templates.list(searchList=[d])
353 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
354 return ({'Status': '303 See Other',
355 'Location': 'info?machine_id=%d' % machine.machine_id},
356 "You shouldn't see this message.")
358 raise InvalidInput('back', back, 'Not a known back page.')
360 def modifyDict(username, state, fields):
361 """Modify a machine as specified by CGI arguments.
363 Return a list of local variables for modify.tmpl.
368 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
369 validate = validation.Validate(username, state, **kws)
370 machine = validate.machine
371 oldname = machine.name
373 if hasattr(validate, 'memory'):
374 machine.memory = validate.memory
376 if hasattr(validate, 'vmtype'):
377 machine.type = validate.vmtype
379 if hasattr(validate, 'disksize'):
380 disksize = validate.disksize
381 disk = machine.disks[0]
382 if disk.size != disksize:
383 olddisk[disk.guest_device_name] = disksize
385 session.save_or_update(disk)
388 if hasattr(validate, 'owner') and validate.owner != machine.owner:
389 machine.owner = validate.owner
391 if hasattr(validate, 'name'):
392 machine.name = validate.name
393 for n in machine.nics:
394 if n.hostname == oldname:
395 n.hostname = validate.name
396 if hasattr(validate, 'description'):
397 machine.description = validate.description
398 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
399 machine.administrator = validate.admin
401 if hasattr(validate, 'contact'):
402 machine.contact = validate.contact
404 session.save_or_update(machine)
406 cache_acls.refreshMachine(machine)
411 for diskname in olddisk:
412 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
413 if hasattr(validate, 'name'):
414 controls.renameMachine(machine, oldname, validate.name)
415 return dict(user=username,
419 def modify(username, state, path, fields):
420 """Handler for modifying attributes of a machine."""
422 modify_dict = modifyDict(username, state, fields)
423 except InvalidInput, err:
425 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
427 machine = modify_dict['machine']
430 info_dict = infoDict(username, state, machine)
431 info_dict['err'] = err
433 for field in fields.keys():
434 setattr(info_dict['defaults'], field, fields.getfirst(field))
435 info_dict['result'] = result
436 return templates.info(searchList=[info_dict])
439 def helpHandler(username, state, path, fields):
440 """Handler for help messages."""
441 simple = fields.getfirst('simple')
442 subjects = fields.getlist('subject')
446 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
447 ParaVM. You can access the resulting system by logging into the <a
448 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
449 with your Kerberos tickets; there is no root password so sshd will
452 <p>Under the covers, the autoinstaller uses our own patched version of
453 xen-create-image, which is a tool based on debootstrap. If you log
454 into the serial console while the install is running, you can watch
457 'ParaVM Console': """
458 ParaVM machines do not support local console access over VNC. To
459 access the serial console of these machines, you can SSH with Kerberos
460 to %s, using the name of the machine as your
461 username.""" % config.console.hostname,
463 HVM machines use the virtualization features of the processor, while
464 ParaVM machines rely on a modified kernel to communicate directly with
465 the hypervisor. HVMs support boot CDs of any operating system, and
466 the VNC console applet. The three-minute autoinstaller produces
467 ParaVMs. ParaVMs typically are more efficient, and always support the
468 <a href="help?subject=ParaVM+Console">console server</a>.</p>
470 <p>More details are <a
471 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
472 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
473 (which you can skip by using the autoinstaller to begin with.)</p>
475 <p>We recommend using a ParaVM when possible and an HVM when necessary.
478 Don't ask us! We're as mystified as you are.""",
480 The owner field is used to determine <a
481 href="help?subject=Quotas">quotas</a>. It must be the name of a
482 locker that you are an AFS administrator of. In particular, you or an
483 AFS group you are a member of must have AFS rlidwka bits on the
484 locker. You can check who administers the LOCKER locker using the
485 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
486 href="help?subject=Administrator">administrator</a>.""",
488 The administrator field determines who can access the console and
489 power on and off the machine. This can be either a user or a moira
492 Quotas are determined on a per-locker basis. Each locker may have a
493 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
496 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
497 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
498 your machine will run just fine, but the applet's display of the
499 console will suffer artifacts.
502 <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>
503 <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.
508 subjects = sorted(help_mapping.keys())
510 d = dict(user=username,
513 mapping=help_mapping)
515 return templates.help(searchList=[d])
518 def badOperation(u, s, p, e):
519 """Function called when accessing an unknown URI."""
520 return ({'Status': '404 Not Found'}, 'Invalid operation.')
522 def infoDict(username, state, machine):
523 """Get the variables used by info.tmpl."""
524 status = controls.statusInfo(machine)
525 checkpoint.checkpoint('Getting status info')
526 has_vnc = hasVnc(status)
528 main_status = dict(name=machine.name,
529 memory=str(machine.memory))
533 main_status = dict(status[1:])
534 main_status['host'] = controls.listHost(machine)
535 start_time = float(main_status.get('start_time', 0))
536 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
537 cpu_time_float = float(main_status.get('cpu_time', 0))
538 cputime = datetime.timedelta(seconds=int(cpu_time_float))
539 checkpoint.checkpoint('Status')
540 display_fields = [('name', 'Name'),
541 ('description', 'Description'),
543 ('administrator', 'Administrator'),
544 ('contact', 'Contact'),
547 ('uptime', 'uptime'),
548 ('cputime', 'CPU usage'),
549 ('host', 'Hosted on'),
552 ('state', 'state (xen format)'),
553 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
557 machine_info['name'] = machine.name
558 machine_info['description'] = machine.description
559 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
560 machine_info['owner'] = machine.owner
561 machine_info['administrator'] = machine.administrator
562 machine_info['contact'] = machine.contact
564 nic_fields = getNicInfo(machine_info, machine)
565 nic_point = display_fields.index('NIC_INFO')
566 display_fields = (display_fields[:nic_point] + nic_fields +
567 display_fields[nic_point+1:])
569 disk_fields = getDiskInfo(machine_info, machine)
570 disk_point = display_fields.index('DISK_INFO')
571 display_fields = (display_fields[:disk_point] + disk_fields +
572 display_fields[disk_point+1:])
574 main_status['memory'] += ' MiB'
575 for field, disp in display_fields:
576 if field in ('uptime', 'cputime') and locals()[field] is not None:
577 fields.append((disp, locals()[field]))
578 elif field in machine_info:
579 fields.append((disp, machine_info[field]))
580 elif field in main_status:
581 fields.append((disp, main_status[field]))
584 #fields.append((disp, None))
586 checkpoint.checkpoint('Got fields')
589 max_mem = validation.maxMemory(machine.owner, state, machine, False)
590 checkpoint.checkpoint('Got mem')
591 max_disk = validation.maxDisk(machine.owner, machine)
592 defaults = Defaults()
593 for name in 'machine_id name description administrator owner memory contact'.split():
594 setattr(defaults, name, getattr(machine, name))
595 defaults.type = machine.type.type_id
596 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
597 checkpoint.checkpoint('Got defaults')
598 d = dict(user=username,
599 on=status is not None,
607 owner_help=helppopup("Owner"),
611 def info(username, state, path, fields):
612 """Handler for info on a single VM."""
613 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
614 d = infoDict(username, state, machine)
615 checkpoint.checkpoint('Got infodict')
616 return templates.info(searchList=[d])
618 def unauthFront(_, _2, _3, fields):
619 """Information for unauth'd users."""
620 return templates.unauth(searchList=[{'simple' : True,
621 'hostname' : socket.getfqdn()}])
623 def admin(username, state, path, fields):
625 return ({'Status': '303 See Other',
626 'Location': 'admin/'},
627 "You shouldn't see this message.")
628 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
629 raise InvalidInput('username', username,
630 'Not in admin group %s.' % config.adminacl)
631 newstate = State(username, isadmin=True)
632 newstate.environ = state.environ
633 return handler(username, newstate, path, fields)
635 def throwError(_, __, ___, ____):
636 """Throw an error, to test the error-tracing mechanisms."""
637 raise RuntimeError("test of the emergency broadcast system")
639 mapping = dict(#list=listVms,
649 errortest=throwError)
651 def printHeaders(headers):
652 """Print a dictionary as HTTP headers."""
653 for key, value in headers.iteritems():
654 print '%s: %s' % (key, value)
657 def send_error_mail(subject, body):
660 to = config.web.errormail
666 """ % (to, config.web.hostname, subject, body)
667 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
668 stdin=subprocess.PIPE)
673 def show_error(op, username, fields, err, emsg, traceback):
674 """Print an error page when an exception occurs"""
675 d = dict(op=op, user=username, fields=fields,
676 errorMessage=str(err), stderr=emsg, traceback=traceback)
677 details = templates.error_raw(searchList=[d])
678 exclude = config.web.errormail_exclude
679 if username not in exclude and '*' not in exclude:
680 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
682 d['details'] = details
683 return templates.error(searchList=[d])
685 def handler(username, state, path, fields):
686 operation, path = pathSplit(path)
689 print 'Starting', operation
690 fun = mapping.get(operation, badOperation)
691 return fun(username, state, path, fields)
694 def __init__(self, environ, start_response):
695 self.environ = environ
696 self.start = start_response
698 self.username = getUser(environ)
699 self.state = State(self.username)
700 self.state.environ = environ
705 start_time = time.time()
706 database.clear_cache()
707 sys.stderr = StringIO()
708 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
709 operation = self.environ.get('PATH_INFO', '')
711 self.start("301 Moved Permanently", [('Location', './')])
713 if self.username is None:
717 checkpoint.checkpoint('Before')
718 output = handler(self.username, self.state, operation, fields)
719 checkpoint.checkpoint('After')
721 headers = dict(DEFAULT_HEADERS)
722 if isinstance(output, tuple):
723 new_headers, output = output
724 headers.update(new_headers)
725 e = revertStandardError()
727 if hasattr(output, 'addError'):
730 # This only happens on redirects, so it'd be a pain to get
731 # the message to the user. Maybe in the response is useful.
732 output = output + '\n\nstderr:\n' + e
733 output_string = str(output)
734 checkpoint.checkpoint('output as a string')
735 except Exception, err:
736 if not fields.has_key('js'):
737 if isinstance(err, InvalidInput):
738 self.start('200 OK', [('Content-Type', 'text/html')])
739 e = revertStandardError()
740 yield str(invalidInput(operation, self.username, fields,
744 self.start('500 Internal Server Error',
745 [('Content-Type', 'text/html')])
746 e = revertStandardError()
747 s = show_error(operation, self.username, fields,
748 err, e, traceback.format_exc())
751 status = headers.setdefault('Status', '200 OK')
752 del headers['Status']
753 self.start(status, headers.items())
755 if fields.has_key('timedebug'):
756 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
763 from flup.server.fcgi_fork import WSGIServer
764 WSGIServer(constructor()).run()
766 if __name__ == '__main__':