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(user=cherrypy.request.login,
145 mapping=help_mapping)
146 help._cp_config['tools.require_login.on'] = False
149 @cherrypy.tools.mako(filename="/helloworld.mako")
150 def helloworld(self, **kwargs):
151 return {'request': cherrypy.request, 'kwargs': kwargs}
152 helloworld._cp_config['tools.require_login.on'] = False
155 if path.startswith('/'):
160 return path[:i], path[i:]
164 self.start_time = time.time()
165 self.checkpoints = []
167 def checkpoint(self, s):
168 self.checkpoints.append((s, time.time()))
171 return ('Timing info:\n%s\n' %
172 '\n'.join(['%s: %s' % (d, t - self.start_time) for
173 (d, t) in self.checkpoints]))
175 checkpoint = Checkpoint()
177 def makeErrorPre(old, addition):
181 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
183 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
185 Template.database = database
186 Template.config = config
190 """Class to store a dictionary that will be converted to JSON"""
191 def __init__(self, **kws):
199 return simplejson.dumps(self.data)
201 def addError(self, text):
202 """Add stderr text to be displayed on the website."""
204 makeErrorPre(self.data.get('err'), text)
207 """Class to store default values for fields."""
216 def __init__(self, max_memory=None, max_disk=None, **kws):
217 if max_memory is not None:
218 self.memory = min(self.memory, max_memory)
219 if max_disk is not None:
220 self.disk = min(self.disk, max_disk)
222 setattr(self, key, kws[key])
226 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
228 def invalidInput(op, username, fields, err, emsg):
229 """Print an error page when an InvalidInput exception occurs"""
230 d = dict(op=op, user=username, err_field=err.err_field,
231 err_value=str(err.err_value), stderr=emsg,
232 errorMessage=str(err))
233 return templates.invalid(searchList=[d])
236 """Does the machine with a given status list support VNC?"""
240 if l[0] == 'device' and l[1][0] == 'vfb':
242 return 'location' in d
245 def parseCreate(username, state, fields):
246 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
247 validate = validation.Validate(username, state, strict=True, **kws)
248 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
249 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
250 cdrom=getattr(validate, 'cdrom', None),
251 autoinstall=getattr(validate, 'autoinstall', None))
253 def create(username, state, path, fields):
254 """Handler for create requests."""
256 parsed_fields = parseCreate(username, state, fields)
257 machine = controls.createVm(username, state, **parsed_fields)
258 except InvalidInput, err:
262 state.clear() #Changed global state
263 d = getListDict(username, state)
266 for field in fields.keys():
267 setattr(d['defaults'], field, fields.getfirst(field))
269 d['new_machine'] = parsed_fields['name']
270 return templates.list(searchList=[d])
273 def getListDict(username, state):
274 """Gets the list of local variables used by list.tmpl."""
275 checkpoint.checkpoint('Starting')
276 machines = state.machines
277 checkpoint.checkpoint('Got my machines')
280 xmlist = state.xmlist
281 checkpoint.checkpoint('Got uptimes')
282 can_clone = 'ice3' not in state.xmlist_raw
288 m.uptime = xmlist[m]['uptime']
289 if xmlist[m]['console']:
294 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
295 max_memory = validation.maxMemory(username, state)
296 max_disk = validation.maxDisk(username)
297 checkpoint.checkpoint('Got max mem/disk')
298 defaults = Defaults(max_memory=max_memory,
301 checkpoint.checkpoint('Got defaults')
302 def sortkey(machine):
303 return (machine.owner != username, machine.owner, machine.name)
304 machines = sorted(machines, key=sortkey)
305 d = dict(user=username,
306 cant_add_vm=validation.cantAddVm(username, state),
307 max_memory=max_memory,
315 def vnc(username, state, path, fields):
318 Note that due to same-domain restrictions, the applet connects to
319 the webserver, which needs to forward those requests to the xen
320 server. The Xen server runs another proxy that (1) authenticates
321 and (2) finds the correct port for the VM.
323 You might want iptables like:
325 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
326 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
327 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
328 --dport 10003 -j SNAT --to-source 18.187.7.142
329 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
330 --dport 10003 -j ACCEPT
332 Remember to enable iptables!
333 echo 1 > /proc/sys/net/ipv4/ip_forward
335 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
337 token = controls.vnctoken(machine)
338 host = controls.listHost(machine)
340 port = 10003 + [h.hostname for h in config.hosts].index(host)
344 status = controls.statusInfo(machine)
345 has_vnc = hasVnc(status)
347 d = dict(user=username,
351 hostname=state.environ.get('SERVER_NAME', 'localhost'),
354 return templates.vnc(searchList=[d])
356 def getHostname(nic):
357 """Find the hostname associated with a NIC.
359 XXX this should be merged with the similar logic in DNS and DHCP.
362 hostname = nic.hostname
364 hostname = nic.machine.name
370 return hostname + '.' + config.dns.domains[0]
372 def getNicInfo(data_dict, machine):
373 """Helper function for info, get data on nics for a machine.
375 Modifies data_dict to include the relevant data, and returns a list
376 of (key, name) pairs to display "name: data_dict[key]" to the user.
378 data_dict['num_nics'] = len(machine.nics)
379 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
380 ('nic%s_mac', 'NIC %s MAC Addr'),
381 ('nic%s_ip', 'NIC %s IP'),
384 for i in range(len(machine.nics)):
385 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
386 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
387 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
388 data_dict['nic%s_ip' % i] = machine.nics[i].ip
389 if len(machine.nics) == 1:
390 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
393 def getDiskInfo(data_dict, machine):
394 """Helper function for info, get data on disks for a machine.
396 Modifies data_dict to include the relevant data, and returns a list
397 of (key, name) pairs to display "name: data_dict[key]" to the user.
399 data_dict['num_disks'] = len(machine.disks)
400 disk_fields_template = [('%s_size', '%s size')]
402 for disk in machine.disks:
403 name = disk.guest_device_name
404 disk_fields.extend([(x % name, y % name) for x, y in
405 disk_fields_template])
406 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
409 def command(username, state, path, fields):
410 """Handler for running commands like boot and delete on a VM."""
411 back = fields.getfirst('back')
413 d = controls.commandResult(username, state, fields)
414 if d['command'] == 'Delete VM':
416 except InvalidInput, err:
419 print >> sys.stderr, err
424 return templates.command(searchList=[d])
426 state.clear() #Changed global state
427 d = getListDict(username, state)
429 return templates.list(searchList=[d])
431 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
432 return ({'Status': '303 See Other',
433 'Location': 'info?machine_id=%d' % machine.machine_id},
434 "You shouldn't see this message.")
436 raise InvalidInput('back', back, 'Not a known back page.')
438 def modifyDict(username, state, fields):
439 """Modify a machine as specified by CGI arguments.
441 Return a list of local variables for modify.tmpl.
446 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
447 validate = validation.Validate(username, state, **kws)
448 machine = validate.machine
449 oldname = machine.name
451 if hasattr(validate, 'memory'):
452 machine.memory = validate.memory
454 if hasattr(validate, 'vmtype'):
455 machine.type = validate.vmtype
457 if hasattr(validate, 'disksize'):
458 disksize = validate.disksize
459 disk = machine.disks[0]
460 if disk.size != disksize:
461 olddisk[disk.guest_device_name] = disksize
463 session.save_or_update(disk)
466 if hasattr(validate, 'owner') and validate.owner != machine.owner:
467 machine.owner = validate.owner
469 if hasattr(validate, 'name'):
470 machine.name = validate.name
471 for n in machine.nics:
472 if n.hostname == oldname:
473 n.hostname = validate.name
474 if hasattr(validate, 'description'):
475 machine.description = validate.description
476 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
477 machine.administrator = validate.admin
479 if hasattr(validate, 'contact'):
480 machine.contact = validate.contact
482 session.save_or_update(machine)
484 cache_acls.refreshMachine(machine)
489 for diskname in olddisk:
490 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
491 if hasattr(validate, 'name'):
492 controls.renameMachine(machine, oldname, validate.name)
493 return dict(user=username,
497 def modify(username, state, path, fields):
498 """Handler for modifying attributes of a machine."""
500 modify_dict = modifyDict(username, state, fields)
501 except InvalidInput, err:
503 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
505 machine = modify_dict['machine']
508 info_dict = infoDict(username, state, machine)
509 info_dict['err'] = err
511 for field in fields.keys():
512 setattr(info_dict['defaults'], field, fields.getfirst(field))
513 info_dict['result'] = result
514 return templates.info(searchList=[info_dict])
516 def badOperation(u, s, p, e):
517 """Function called when accessing an unknown URI."""
518 return ({'Status': '404 Not Found'}, 'Invalid operation.')
520 def infoDict(username, state, machine):
521 """Get the variables used by info.tmpl."""
522 status = controls.statusInfo(machine)
523 checkpoint.checkpoint('Getting status info')
524 has_vnc = hasVnc(status)
526 main_status = dict(name=machine.name,
527 memory=str(machine.memory))
531 main_status = dict(status[1:])
532 main_status['host'] = controls.listHost(machine)
533 start_time = float(main_status.get('start_time', 0))
534 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
535 cpu_time_float = float(main_status.get('cpu_time', 0))
536 cputime = datetime.timedelta(seconds=int(cpu_time_float))
537 checkpoint.checkpoint('Status')
538 display_fields = [('name', 'Name'),
539 ('description', 'Description'),
541 ('administrator', 'Administrator'),
542 ('contact', 'Contact'),
545 ('uptime', 'uptime'),
546 ('cputime', 'CPU usage'),
547 ('host', 'Hosted on'),
550 ('state', 'state (xen format)'),
551 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
555 machine_info['name'] = machine.name
556 machine_info['description'] = machine.description
557 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
558 machine_info['owner'] = machine.owner
559 machine_info['administrator'] = machine.administrator
560 machine_info['contact'] = machine.contact
562 nic_fields = getNicInfo(machine_info, machine)
563 nic_point = display_fields.index('NIC_INFO')
564 display_fields = (display_fields[:nic_point] + nic_fields +
565 display_fields[nic_point+1:])
567 disk_fields = getDiskInfo(machine_info, machine)
568 disk_point = display_fields.index('DISK_INFO')
569 display_fields = (display_fields[:disk_point] + disk_fields +
570 display_fields[disk_point+1:])
572 main_status['memory'] += ' MiB'
573 for field, disp in display_fields:
574 if field in ('uptime', 'cputime') and locals()[field] is not None:
575 fields.append((disp, locals()[field]))
576 elif field in machine_info:
577 fields.append((disp, machine_info[field]))
578 elif field in main_status:
579 fields.append((disp, main_status[field]))
582 #fields.append((disp, None))
584 checkpoint.checkpoint('Got fields')
587 max_mem = validation.maxMemory(machine.owner, state, machine, False)
588 checkpoint.checkpoint('Got mem')
589 max_disk = validation.maxDisk(machine.owner, machine)
590 defaults = Defaults()
591 for name in 'machine_id name description administrator owner memory contact'.split():
592 setattr(defaults, name, getattr(machine, name))
593 defaults.type = machine.type.type_id
594 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
595 checkpoint.checkpoint('Got defaults')
596 d = dict(user=username,
597 on=status is not None,
605 owner_help=helppopup("Owner"),
609 def info(username, state, path, fields):
610 """Handler for info on a single VM."""
611 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
612 d = infoDict(username, state, machine)
613 checkpoint.checkpoint('Got infodict')
614 return templates.info(searchList=[d])
616 def unauthFront(_, _2, _3, fields):
617 """Information for unauth'd users."""
618 return templates.unauth(searchList=[{'simple' : True,
619 'hostname' : socket.getfqdn()}])
621 def admin(username, state, path, fields):
623 return ({'Status': '303 See Other',
624 'Location': 'admin/'},
625 "You shouldn't see this message.")
626 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
627 raise InvalidInput('username', username,
628 'Not in admin group %s.' % config.adminacl)
629 newstate = State(username, isadmin=True)
630 newstate.environ = state.environ
631 return handler(username, newstate, path, fields)
633 def throwError(_, __, ___, ____):
634 """Throw an error, to test the error-tracing mechanisms."""
635 raise RuntimeError("test of the emergency broadcast system")
637 mapping = dict(vnc=vnc,
645 errortest=throwError)
647 def printHeaders(headers):
648 """Print a dictionary as HTTP headers."""
649 for key, value in headers.iteritems():
650 print '%s: %s' % (key, value)
653 def send_error_mail(subject, body):
656 to = config.web.errormail
662 """ % (to, config.web.hostname, subject, body)
663 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
664 stdin=subprocess.PIPE)
669 def show_error(op, username, fields, err, emsg, traceback):
670 """Print an error page when an exception occurs"""
671 d = dict(op=op, user=username, fields=fields,
672 errorMessage=str(err), stderr=emsg, traceback=traceback)
673 details = templates.error_raw(searchList=[d])
674 exclude = config.web.errormail_exclude
675 if username not in exclude and '*' not in exclude:
676 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
678 d['details'] = details
679 return templates.error(searchList=[d])
681 def handler(username, state, path, fields):
682 operation, path = pathSplit(path)
685 print 'Starting', operation
686 fun = mapping.get(operation, badOperation)
687 return fun(username, state, path, fields)
690 def __init__(self, environ, start_response):
691 self.environ = environ
692 self.start = start_response
694 self.username = getUser(environ)
695 self.state = State(self.username)
696 self.state.environ = environ
701 start_time = time.time()
702 database.clear_cache()
703 sys.stderr = StringIO()
704 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
705 operation = self.environ.get('PATH_INFO', '')
707 self.start("301 Moved Permanently", [('Location', './')])
709 if self.username is None:
713 checkpoint.checkpoint('Before')
714 output = handler(self.username, self.state, operation, fields)
715 checkpoint.checkpoint('After')
717 headers = dict(DEFAULT_HEADERS)
718 if isinstance(output, tuple):
719 new_headers, output = output
720 headers.update(new_headers)
721 e = revertStandardError()
723 if hasattr(output, 'addError'):
726 # This only happens on redirects, so it'd be a pain to get
727 # the message to the user. Maybe in the response is useful.
728 output = output + '\n\nstderr:\n' + e
729 output_string = str(output)
730 checkpoint.checkpoint('output as a string')
731 except Exception, err:
732 if not fields.has_key('js'):
733 if isinstance(err, InvalidInput):
734 self.start('200 OK', [('Content-Type', 'text/html')])
735 e = revertStandardError()
736 yield str(invalidInput(operation, self.username, fields,
740 self.start('500 Internal Server Error',
741 [('Content-Type', 'text/html')])
742 e = revertStandardError()
743 s = show_error(operation, self.username, fields,
744 err, e, traceback.format_exc())
747 status = headers.setdefault('Status', '200 OK')
748 del headers['Status']
749 self.start(status, headers.items())
751 if fields.has_key('timedebug'):
752 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
759 from flup.server.fcgi_fork import WSGIServer
760 WSGIServer(constructor()).run()
762 if __name__ == '__main__':