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
153 class MachineView(View):
154 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
155 # _cp_dispatch, and parse the argument as a list instead of
158 def __getattr__(self, name):
160 machine_id = int(name)
161 cherrypy.request.params['machine_id'] = machine_id
167 @cherrypy.tools.mako(filename="/info.mako")
168 def info(self, machine_id):
169 """Handler for info on a single VM."""
170 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
171 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
172 checkpoint.checkpoint('Got infodict')
177 @cherrypy.tools.mako(filename="/vnc.mako")
178 def vnc(self, machine_id):
181 Note that due to same-domain restrictions, the applet connects to
182 the webserver, which needs to forward those requests to the xen
183 server. The Xen server runs another proxy that (1) authenticates
184 and (2) finds the correct port for the VM.
186 You might want iptables like:
188 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
189 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
190 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
191 --dport 10003 -j SNAT --to-source 18.187.7.142
192 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
193 --dport 10003 -j ACCEPT
195 Remember to enable iptables!
196 echo 1 > /proc/sys/net/ipv4/ip_forward
198 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
200 token = controls.vnctoken(machine)
201 host = controls.listHost(machine)
203 port = 10003 + [h.hostname for h in config.hosts].index(host)
207 status = controls.statusInfo(machine)
208 has_vnc = hasVnc(status)
213 hostname=cherrypy.request.local.name,
218 machine = MachineView()
221 if path.startswith('/'):
226 return path[:i], path[i:]
230 self.start_time = time.time()
231 self.checkpoints = []
233 def checkpoint(self, s):
234 self.checkpoints.append((s, time.time()))
237 return ('Timing info:\n%s\n' %
238 '\n'.join(['%s: %s' % (d, t - self.start_time) for
239 (d, t) in self.checkpoints]))
241 checkpoint = Checkpoint()
243 def makeErrorPre(old, addition):
247 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
249 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
251 Template.database = database
252 Template.config = config
256 """Class to store a dictionary that will be converted to JSON"""
257 def __init__(self, **kws):
265 return simplejson.dumps(self.data)
267 def addError(self, text):
268 """Add stderr text to be displayed on the website."""
270 makeErrorPre(self.data.get('err'), text)
273 """Class to store default values for fields."""
282 def __init__(self, max_memory=None, max_disk=None, **kws):
283 if max_memory is not None:
284 self.memory = min(self.memory, max_memory)
285 if max_disk is not None:
286 self.disk = min(self.disk, max_disk)
288 setattr(self, key, kws[key])
292 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
294 def invalidInput(op, username, fields, err, emsg):
295 """Print an error page when an InvalidInput exception occurs"""
296 d = dict(op=op, user=username, err_field=err.err_field,
297 err_value=str(err.err_value), stderr=emsg,
298 errorMessage=str(err))
299 return templates.invalid(searchList=[d])
302 """Does the machine with a given status list support VNC?"""
306 if l[0] == 'device' and l[1][0] == 'vfb':
308 return 'location' in d
311 def parseCreate(username, state, fields):
312 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
313 validate = validation.Validate(username, state, strict=True, **kws)
314 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
315 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
316 cdrom=getattr(validate, 'cdrom', None),
317 autoinstall=getattr(validate, 'autoinstall', None))
319 def create(username, state, path, fields):
320 """Handler for create requests."""
322 parsed_fields = parseCreate(username, state, fields)
323 machine = controls.createVm(username, state, **parsed_fields)
324 except InvalidInput, err:
328 state.clear() #Changed global state
329 d = getListDict(username, state)
332 for field in fields.keys():
333 setattr(d['defaults'], field, fields.getfirst(field))
335 d['new_machine'] = parsed_fields['name']
336 return templates.list(searchList=[d])
339 def getListDict(username, state):
340 """Gets the list of local variables used by list.tmpl."""
341 checkpoint.checkpoint('Starting')
342 machines = state.machines
343 checkpoint.checkpoint('Got my machines')
346 xmlist = state.xmlist
347 checkpoint.checkpoint('Got uptimes')
348 can_clone = 'ice3' not in state.xmlist_raw
354 m.uptime = xmlist[m]['uptime']
355 if xmlist[m]['console']:
360 has_vnc[m] = "ParaVM"
361 max_memory = validation.maxMemory(username, state)
362 max_disk = validation.maxDisk(username)
363 checkpoint.checkpoint('Got max mem/disk')
364 defaults = Defaults(max_memory=max_memory,
367 checkpoint.checkpoint('Got defaults')
368 def sortkey(machine):
369 return (machine.owner != username, machine.owner, machine.name)
370 machines = sorted(machines, key=sortkey)
371 d = dict(user=username,
372 cant_add_vm=validation.cantAddVm(username, state),
373 max_memory=max_memory,
381 def getHostname(nic):
382 """Find the hostname associated with a NIC.
384 XXX this should be merged with the similar logic in DNS and DHCP.
387 hostname = nic.hostname
389 hostname = nic.machine.name
395 return hostname + '.' + config.dns.domains[0]
397 def getNicInfo(data_dict, machine):
398 """Helper function for info, get data on nics for a machine.
400 Modifies data_dict to include the relevant data, and returns a list
401 of (key, name) pairs to display "name: data_dict[key]" to the user.
403 data_dict['num_nics'] = len(machine.nics)
404 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
405 ('nic%s_mac', 'NIC %s MAC Addr'),
406 ('nic%s_ip', 'NIC %s IP'),
409 for i in range(len(machine.nics)):
410 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
411 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
412 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
413 data_dict['nic%s_ip' % i] = machine.nics[i].ip
414 if len(machine.nics) == 1:
415 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
418 def getDiskInfo(data_dict, machine):
419 """Helper function for info, get data on disks for a machine.
421 Modifies data_dict to include the relevant data, and returns a list
422 of (key, name) pairs to display "name: data_dict[key]" to the user.
424 data_dict['num_disks'] = len(machine.disks)
425 disk_fields_template = [('%s_size', '%s size')]
427 for disk in machine.disks:
428 name = disk.guest_device_name
429 disk_fields.extend([(x % name, y % name) for x, y in
430 disk_fields_template])
431 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
434 def command(username, state, path, fields):
435 """Handler for running commands like boot and delete on a VM."""
436 back = fields.getfirst('back')
438 d = controls.commandResult(username, state, fields)
439 if d['command'] == 'Delete VM':
441 except InvalidInput, err:
444 print >> sys.stderr, err
449 return templates.command(searchList=[d])
451 state.clear() #Changed global state
452 d = getListDict(username, state)
454 return templates.list(searchList=[d])
456 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
457 return ({'Status': '303 See Other',
458 'Location': 'info?machine_id=%d' % machine.machine_id},
459 "You shouldn't see this message.")
461 raise InvalidInput('back', back, 'Not a known back page.')
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")
660 errortest=throwError)
662 def printHeaders(headers):
663 """Print a dictionary as HTTP headers."""
664 for key, value in headers.iteritems():
665 print '%s: %s' % (key, value)
668 def send_error_mail(subject, body):
671 to = config.web.errormail
677 """ % (to, config.web.hostname, subject, body)
678 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
679 stdin=subprocess.PIPE)
684 def show_error(op, username, fields, err, emsg, traceback):
685 """Print an error page when an exception occurs"""
686 d = dict(op=op, user=username, fields=fields,
687 errorMessage=str(err), stderr=emsg, traceback=traceback)
688 details = templates.error_raw(searchList=[d])
689 exclude = config.web.errormail_exclude
690 if username not in exclude and '*' not in exclude:
691 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
693 d['details'] = details
694 return templates.error(searchList=[d])
696 def handler(username, state, path, fields):
697 operation, path = pathSplit(path)
700 print 'Starting', operation
701 fun = mapping.get(operation, badOperation)
702 return fun(username, state, path, fields)
705 def __init__(self, environ, start_response):
706 self.environ = environ
707 self.start = start_response
709 self.username = getUser(environ)
710 self.state = State(self.username)
711 self.state.environ = environ
716 start_time = time.time()
717 database.clear_cache()
718 sys.stderr = StringIO()
719 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
720 operation = self.environ.get('PATH_INFO', '')
722 self.start("301 Moved Permanently", [('Location', './')])
724 if self.username is None:
728 checkpoint.checkpoint('Before')
729 output = handler(self.username, self.state, operation, fields)
730 checkpoint.checkpoint('After')
732 headers = dict(DEFAULT_HEADERS)
733 if isinstance(output, tuple):
734 new_headers, output = output
735 headers.update(new_headers)
736 e = revertStandardError()
738 if hasattr(output, 'addError'):
741 # This only happens on redirects, so it'd be a pain to get
742 # the message to the user. Maybe in the response is useful.
743 output = output + '\n\nstderr:\n' + e
744 output_string = str(output)
745 checkpoint.checkpoint('output as a string')
746 except Exception, err:
747 if not fields.has_key('js'):
748 if isinstance(err, InvalidInput):
749 self.start('200 OK', [('Content-Type', 'text/html')])
750 e = revertStandardError()
751 yield str(invalidInput(operation, self.username, fields,
755 self.start('500 Internal Server Error',
756 [('Content-Type', 'text/html')])
757 e = revertStandardError()
758 s = show_error(operation, self.username, fields,
759 err, e, traceback.format_exc())
762 status = headers.setdefault('Status', '200 OK')
763 del headers['Status']
764 self.start(status, headers.items())
766 if fields.has_key('timedebug'):
767 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
774 from flup.server.fcgi_fork import WSGIServer
775 WSGIServer(constructor()).run()
777 if __name__ == '__main__':