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="/help.mako")
71 def help(self, subject=None, simple=False):
72 """Handler for help messages."""
76 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
77 ParaVM. You can access the resulting system by logging into the <a
78 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
79 with your Kerberos tickets; there is no root password so sshd will
82 <p>Under the covers, the autoinstaller uses our own patched version of
83 xen-create-image, which is a tool based on debootstrap. If you log
84 into the serial console while the install is running, you can watch
88 ParaVM machines do not support local console access over VNC. To
89 access the serial console of these machines, you can SSH with Kerberos
90 to %s, using the name of the machine as your
91 username.""" % config.console.hostname,
93 HVM machines use the virtualization features of the processor, while
94 ParaVM machines rely on a modified kernel to communicate directly with
95 the hypervisor. HVMs support boot CDs of any operating system, and
96 the VNC console applet. The three-minute autoinstaller produces
97 ParaVMs. ParaVMs typically are more efficient, and always support the
98 <a href="help?subject=ParaVM+Console">console server</a>.</p>
100 <p>More details are <a
101 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
102 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
103 (which you can skip by using the autoinstaller to begin with.)</p>
105 <p>We recommend using a ParaVM when possible and an HVM when necessary.
108 Don't ask us! We're as mystified as you are.""",
110 The owner field is used to determine <a
111 href="help?subject=Quotas">quotas</a>. It must be the name of a
112 locker that you are an AFS administrator of. In particular, you or an
113 AFS group you are a member of must have AFS rlidwka bits on the
114 locker. You can check who administers the LOCKER locker using the
115 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
116 href="help?subject=Administrator">administrator</a>.""",
118 The administrator field determines who can access the console and
119 power on and off the machine. This can be either a user or a moira
122 Quotas are determined on a per-locker basis. Each locker may have a
123 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
126 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
127 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
128 your machine will run just fine, but the applet's display of the
129 console will suffer artifacts.
132 <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>
133 <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.
138 subject = sorted(help_mapping.keys())
139 if not isinstance(subject, list):
142 return dict(simple=simple,
144 mapping=help_mapping)
145 help._cp_config['tools.require_login.on'] = False
148 @cherrypy.tools.mako(filename="/helloworld.mako")
149 def helloworld(self, **kwargs):
150 return {'request': cherrypy.request, 'kwargs': kwargs}
151 helloworld._cp_config['tools.require_login.on'] = False
154 if path.startswith('/'):
159 return path[:i], path[i:]
163 self.start_time = time.time()
164 self.checkpoints = []
166 def checkpoint(self, s):
167 self.checkpoints.append((s, time.time()))
170 return ('Timing info:\n%s\n' %
171 '\n'.join(['%s: %s' % (d, t - self.start_time) for
172 (d, t) in self.checkpoints]))
174 checkpoint = Checkpoint()
176 def makeErrorPre(old, addition):
180 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
182 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
184 Template.database = database
185 Template.config = config
189 """Class to store a dictionary that will be converted to JSON"""
190 def __init__(self, **kws):
198 return simplejson.dumps(self.data)
200 def addError(self, text):
201 """Add stderr text to be displayed on the website."""
203 makeErrorPre(self.data.get('err'), text)
206 """Class to store default values for fields."""
215 def __init__(self, max_memory=None, max_disk=None, **kws):
216 if max_memory is not None:
217 self.memory = min(self.memory, max_memory)
218 if max_disk is not None:
219 self.disk = min(self.disk, max_disk)
221 setattr(self, key, kws[key])
225 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
227 def invalidInput(op, username, fields, err, emsg):
228 """Print an error page when an InvalidInput exception occurs"""
229 d = dict(op=op, user=username, err_field=err.err_field,
230 err_value=str(err.err_value), stderr=emsg,
231 errorMessage=str(err))
232 return templates.invalid(searchList=[d])
235 """Does the machine with a given status list support VNC?"""
239 if l[0] == 'device' and l[1][0] == 'vfb':
241 return 'location' in d
244 def parseCreate(username, state, fields):
245 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
246 validate = validation.Validate(username, state, strict=True, **kws)
247 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
248 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
249 cdrom=getattr(validate, 'cdrom', None),
250 autoinstall=getattr(validate, 'autoinstall', None))
252 def create(username, state, path, fields):
253 """Handler for create requests."""
255 parsed_fields = parseCreate(username, state, fields)
256 machine = controls.createVm(username, state, **parsed_fields)
257 except InvalidInput, err:
261 state.clear() #Changed global state
262 d = getListDict(username, state)
265 for field in fields.keys():
266 setattr(d['defaults'], field, fields.getfirst(field))
268 d['new_machine'] = parsed_fields['name']
269 return templates.list(searchList=[d])
272 def getListDict(username, state):
273 """Gets the list of local variables used by list.tmpl."""
274 checkpoint.checkpoint('Starting')
275 machines = state.machines
276 checkpoint.checkpoint('Got my machines')
279 xmlist = state.xmlist
280 checkpoint.checkpoint('Got uptimes')
281 can_clone = 'ice3' not in state.xmlist_raw
287 m.uptime = xmlist[m]['uptime']
288 if xmlist[m]['console']:
293 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
294 max_memory = validation.maxMemory(username, state)
295 max_disk = validation.maxDisk(username)
296 checkpoint.checkpoint('Got max mem/disk')
297 defaults = Defaults(max_memory=max_memory,
300 checkpoint.checkpoint('Got defaults')
301 def sortkey(machine):
302 return (machine.owner != username, machine.owner, machine.name)
303 machines = sorted(machines, key=sortkey)
304 d = dict(user=username,
305 cant_add_vm=validation.cantAddVm(username, state),
306 max_memory=max_memory,
314 def vnc(username, state, path, fields):
317 Note that due to same-domain restrictions, the applet connects to
318 the webserver, which needs to forward those requests to the xen
319 server. The Xen server runs another proxy that (1) authenticates
320 and (2) finds the correct port for the VM.
322 You might want iptables like:
324 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
325 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
326 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
327 --dport 10003 -j SNAT --to-source 18.187.7.142
328 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
329 --dport 10003 -j ACCEPT
331 Remember to enable iptables!
332 echo 1 > /proc/sys/net/ipv4/ip_forward
334 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
336 token = controls.vnctoken(machine)
337 host = controls.listHost(machine)
339 port = 10003 + [h.hostname for h in config.hosts].index(host)
343 status = controls.statusInfo(machine)
344 has_vnc = hasVnc(status)
346 d = dict(user=username,
350 hostname=state.environ.get('SERVER_NAME', 'localhost'),
353 return templates.vnc(searchList=[d])
355 def getHostname(nic):
356 """Find the hostname associated with a NIC.
358 XXX this should be merged with the similar logic in DNS and DHCP.
361 hostname = nic.hostname
363 hostname = nic.machine.name
369 return hostname + '.' + config.dns.domains[0]
371 def getNicInfo(data_dict, machine):
372 """Helper function for info, get data on nics for a machine.
374 Modifies data_dict to include the relevant data, and returns a list
375 of (key, name) pairs to display "name: data_dict[key]" to the user.
377 data_dict['num_nics'] = len(machine.nics)
378 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
379 ('nic%s_mac', 'NIC %s MAC Addr'),
380 ('nic%s_ip', 'NIC %s IP'),
383 for i in range(len(machine.nics)):
384 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
385 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
386 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
387 data_dict['nic%s_ip' % i] = machine.nics[i].ip
388 if len(machine.nics) == 1:
389 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
392 def getDiskInfo(data_dict, machine):
393 """Helper function for info, get data on disks for a machine.
395 Modifies data_dict to include the relevant data, and returns a list
396 of (key, name) pairs to display "name: data_dict[key]" to the user.
398 data_dict['num_disks'] = len(machine.disks)
399 disk_fields_template = [('%s_size', '%s size')]
401 for disk in machine.disks:
402 name = disk.guest_device_name
403 disk_fields.extend([(x % name, y % name) for x, y in
404 disk_fields_template])
405 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
408 def command(username, state, path, fields):
409 """Handler for running commands like boot and delete on a VM."""
410 back = fields.getfirst('back')
412 d = controls.commandResult(username, state, fields)
413 if d['command'] == 'Delete VM':
415 except InvalidInput, err:
418 print >> sys.stderr, err
423 return templates.command(searchList=[d])
425 state.clear() #Changed global state
426 d = getListDict(username, state)
428 return templates.list(searchList=[d])
430 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
431 return ({'Status': '303 See Other',
432 'Location': 'info?machine_id=%d' % machine.machine_id},
433 "You shouldn't see this message.")
435 raise InvalidInput('back', back, 'Not a known back page.')
437 def modifyDict(username, state, fields):
438 """Modify a machine as specified by CGI arguments.
440 Return a list of local variables for modify.tmpl.
445 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
446 validate = validation.Validate(username, state, **kws)
447 machine = validate.machine
448 oldname = machine.name
450 if hasattr(validate, 'memory'):
451 machine.memory = validate.memory
453 if hasattr(validate, 'vmtype'):
454 machine.type = validate.vmtype
456 if hasattr(validate, 'disksize'):
457 disksize = validate.disksize
458 disk = machine.disks[0]
459 if disk.size != disksize:
460 olddisk[disk.guest_device_name] = disksize
462 session.save_or_update(disk)
465 if hasattr(validate, 'owner') and validate.owner != machine.owner:
466 machine.owner = validate.owner
468 if hasattr(validate, 'name'):
469 machine.name = validate.name
470 for n in machine.nics:
471 if n.hostname == oldname:
472 n.hostname = validate.name
473 if hasattr(validate, 'description'):
474 machine.description = validate.description
475 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
476 machine.administrator = validate.admin
478 if hasattr(validate, 'contact'):
479 machine.contact = validate.contact
481 session.save_or_update(machine)
483 cache_acls.refreshMachine(machine)
488 for diskname in olddisk:
489 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
490 if hasattr(validate, 'name'):
491 controls.renameMachine(machine, oldname, validate.name)
492 return dict(user=username,
496 def modify(username, state, path, fields):
497 """Handler for modifying attributes of a machine."""
499 modify_dict = modifyDict(username, state, fields)
500 except InvalidInput, err:
502 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
504 machine = modify_dict['machine']
507 info_dict = infoDict(username, state, machine)
508 info_dict['err'] = err
510 for field in fields.keys():
511 setattr(info_dict['defaults'], field, fields.getfirst(field))
512 info_dict['result'] = result
513 return templates.info(searchList=[info_dict])
515 def badOperation(u, s, p, e):
516 """Function called when accessing an unknown URI."""
517 return ({'Status': '404 Not Found'}, 'Invalid operation.')
519 def infoDict(username, state, machine):
520 """Get the variables used by info.tmpl."""
521 status = controls.statusInfo(machine)
522 checkpoint.checkpoint('Getting status info')
523 has_vnc = hasVnc(status)
525 main_status = dict(name=machine.name,
526 memory=str(machine.memory))
530 main_status = dict(status[1:])
531 main_status['host'] = controls.listHost(machine)
532 start_time = float(main_status.get('start_time', 0))
533 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
534 cpu_time_float = float(main_status.get('cpu_time', 0))
535 cputime = datetime.timedelta(seconds=int(cpu_time_float))
536 checkpoint.checkpoint('Status')
537 display_fields = [('name', 'Name'),
538 ('description', 'Description'),
540 ('administrator', 'Administrator'),
541 ('contact', 'Contact'),
544 ('uptime', 'uptime'),
545 ('cputime', 'CPU usage'),
546 ('host', 'Hosted on'),
549 ('state', 'state (xen format)'),
550 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
554 machine_info['name'] = machine.name
555 machine_info['description'] = machine.description
556 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
557 machine_info['owner'] = machine.owner
558 machine_info['administrator'] = machine.administrator
559 machine_info['contact'] = machine.contact
561 nic_fields = getNicInfo(machine_info, machine)
562 nic_point = display_fields.index('NIC_INFO')
563 display_fields = (display_fields[:nic_point] + nic_fields +
564 display_fields[nic_point+1:])
566 disk_fields = getDiskInfo(machine_info, machine)
567 disk_point = display_fields.index('DISK_INFO')
568 display_fields = (display_fields[:disk_point] + disk_fields +
569 display_fields[disk_point+1:])
571 main_status['memory'] += ' MiB'
572 for field, disp in display_fields:
573 if field in ('uptime', 'cputime') and locals()[field] is not None:
574 fields.append((disp, locals()[field]))
575 elif field in machine_info:
576 fields.append((disp, machine_info[field]))
577 elif field in main_status:
578 fields.append((disp, main_status[field]))
581 #fields.append((disp, None))
583 checkpoint.checkpoint('Got fields')
586 max_mem = validation.maxMemory(machine.owner, state, machine, False)
587 checkpoint.checkpoint('Got mem')
588 max_disk = validation.maxDisk(machine.owner, machine)
589 defaults = Defaults()
590 for name in 'machine_id name description administrator owner memory contact'.split():
591 setattr(defaults, name, getattr(machine, name))
592 defaults.type = machine.type.type_id
593 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
594 checkpoint.checkpoint('Got defaults')
595 d = dict(user=username,
596 on=status is not None,
604 owner_help=helppopup("Owner"),
608 def info(username, state, path, fields):
609 """Handler for info on a single VM."""
610 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
611 d = infoDict(username, state, machine)
612 checkpoint.checkpoint('Got infodict')
613 return templates.info(searchList=[d])
615 def unauthFront(_, _2, _3, fields):
616 """Information for unauth'd users."""
617 return templates.unauth(searchList=[{'simple' : True,
618 'hostname' : socket.getfqdn()}])
620 def admin(username, state, path, fields):
622 return ({'Status': '303 See Other',
623 'Location': 'admin/'},
624 "You shouldn't see this message.")
625 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
626 raise InvalidInput('username', username,
627 'Not in admin group %s.' % config.adminacl)
628 newstate = State(username, isadmin=True)
629 newstate.environ = state.environ
630 return handler(username, newstate, path, fields)
632 def throwError(_, __, ___, ____):
633 """Throw an error, to test the error-tracing mechanisms."""
634 raise RuntimeError("test of the emergency broadcast system")
636 mapping = dict(vnc=vnc,
644 errortest=throwError)
646 def printHeaders(headers):
647 """Print a dictionary as HTTP headers."""
648 for key, value in headers.iteritems():
649 print '%s: %s' % (key, value)
652 def send_error_mail(subject, body):
655 to = config.web.errormail
661 """ % (to, config.web.hostname, subject, body)
662 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
663 stdin=subprocess.PIPE)
668 def show_error(op, username, fields, err, emsg, traceback):
669 """Print an error page when an exception occurs"""
670 d = dict(op=op, user=username, fields=fields,
671 errorMessage=str(err), stderr=emsg, traceback=traceback)
672 details = templates.error_raw(searchList=[d])
673 exclude = config.web.errormail_exclude
674 if username not in exclude and '*' not in exclude:
675 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
677 d['details'] = details
678 return templates.error(searchList=[d])
680 def handler(username, state, path, fields):
681 operation, path = pathSplit(path)
684 print 'Starting', operation
685 fun = mapping.get(operation, badOperation)
686 return fun(username, state, path, fields)
689 def __init__(self, environ, start_response):
690 self.environ = environ
691 self.start = start_response
693 self.username = getUser(environ)
694 self.state = State(self.username)
695 self.state.environ = environ
700 start_time = time.time()
701 database.clear_cache()
702 sys.stderr = StringIO()
703 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
704 operation = self.environ.get('PATH_INFO', '')
706 self.start("301 Moved Permanently", [('Location', './')])
708 if self.username is None:
712 checkpoint.checkpoint('Before')
713 output = handler(self.username, self.state, operation, fields)
714 checkpoint.checkpoint('After')
716 headers = dict(DEFAULT_HEADERS)
717 if isinstance(output, tuple):
718 new_headers, output = output
719 headers.update(new_headers)
720 e = revertStandardError()
722 if hasattr(output, 'addError'):
725 # This only happens on redirects, so it'd be a pain to get
726 # the message to the user. Maybe in the response is useful.
727 output = output + '\n\nstderr:\n' + e
728 output_string = str(output)
729 checkpoint.checkpoint('output as a string')
730 except Exception, err:
731 if not fields.has_key('js'):
732 if isinstance(err, InvalidInput):
733 self.start('200 OK', [('Content-Type', 'text/html')])
734 e = revertStandardError()
735 yield str(invalidInput(operation, self.username, fields,
739 self.start('500 Internal Server Error',
740 [('Content-Type', 'text/html')])
741 e = revertStandardError()
742 s = show_error(operation, self.username, fields,
743 err, e, traceback.format_exc())
746 status = headers.setdefault('Status', '200 OK')
747 del headers['Status']
748 self.start(status, headers.items())
750 if fields.has_key('timedebug'):
751 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
758 from flup.server.fcgi_fork import WSGIServer
759 WSGIServer(constructor()).run()
761 if __name__ == '__main__':