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
57 @cherrypy.tools.mako(filename="/list.mako")
59 """Handler for list requests."""
60 checkpoint.checkpoint('Getting list dict')
61 d = getListDict(cherrypy.request.login, cherrypy.request.state)
62 checkpoint.checkpoint('Got list dict')
67 @cherrypy.tools.mako(filename="/helloworld.mako")
70 return "Hello world!\nYour request: "+repr(dir(cherrypy.request))
71 helloworld._cp_config['tools.require_login.on'] = False
74 if path.startswith('/'):
79 return path[:i], path[i:]
83 self.start_time = time.time()
86 def checkpoint(self, s):
87 self.checkpoints.append((s, time.time()))
90 return ('Timing info:\n%s\n' %
91 '\n'.join(['%s: %s' % (d, t - self.start_time) for
92 (d, t) in self.checkpoints]))
94 checkpoint = Checkpoint()
96 def makeErrorPre(old, addition):
100 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
102 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
104 Template.database = database
105 Template.config = config
109 """Class to store a dictionary that will be converted to JSON"""
110 def __init__(self, **kws):
118 return simplejson.dumps(self.data)
120 def addError(self, text):
121 """Add stderr text to be displayed on the website."""
123 makeErrorPre(self.data.get('err'), text)
126 """Class to store default values for fields."""
135 def __init__(self, max_memory=None, max_disk=None, **kws):
136 if max_memory is not None:
137 self.memory = min(self.memory, max_memory)
138 if max_disk is not None:
139 self.disk = min(self.disk, max_disk)
141 setattr(self, key, kws[key])
145 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
147 def invalidInput(op, username, fields, err, emsg):
148 """Print an error page when an InvalidInput exception occurs"""
149 d = dict(op=op, user=username, err_field=err.err_field,
150 err_value=str(err.err_value), stderr=emsg,
151 errorMessage=str(err))
152 return templates.invalid(searchList=[d])
155 """Does the machine with a given status list support VNC?"""
159 if l[0] == 'device' and l[1][0] == 'vfb':
161 return 'location' in d
164 def parseCreate(username, state, fields):
165 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
166 validate = validation.Validate(username, state, strict=True, **kws)
167 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
168 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
169 cdrom=getattr(validate, 'cdrom', None),
170 autoinstall=getattr(validate, 'autoinstall', None))
172 def create(username, state, path, fields):
173 """Handler for create requests."""
175 parsed_fields = parseCreate(username, state, fields)
176 machine = controls.createVm(username, state, **parsed_fields)
177 except InvalidInput, err:
181 state.clear() #Changed global state
182 d = getListDict(username, state)
185 for field in fields.keys():
186 setattr(d['defaults'], field, fields.getfirst(field))
188 d['new_machine'] = parsed_fields['name']
189 return templates.list(searchList=[d])
192 def getListDict(username, state):
193 """Gets the list of local variables used by list.tmpl."""
194 checkpoint.checkpoint('Starting')
195 machines = state.machines
196 checkpoint.checkpoint('Got my machines')
199 xmlist = state.xmlist
200 checkpoint.checkpoint('Got uptimes')
201 can_clone = 'ice3' not in state.xmlist_raw
207 m.uptime = xmlist[m]['uptime']
208 if xmlist[m]['console']:
213 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
214 max_memory = validation.maxMemory(username, state)
215 max_disk = validation.maxDisk(username)
216 checkpoint.checkpoint('Got max mem/disk')
217 defaults = Defaults(max_memory=max_memory,
220 checkpoint.checkpoint('Got defaults')
221 def sortkey(machine):
222 return (machine.owner != username, machine.owner, machine.name)
223 machines = sorted(machines, key=sortkey)
224 d = dict(user=username,
225 cant_add_vm=validation.cantAddVm(username, state),
226 max_memory=max_memory,
234 def vnc(username, state, path, fields):
237 Note that due to same-domain restrictions, the applet connects to
238 the webserver, which needs to forward those requests to the xen
239 server. The Xen server runs another proxy that (1) authenticates
240 and (2) finds the correct port for the VM.
242 You might want iptables like:
244 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
245 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
246 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
247 --dport 10003 -j SNAT --to-source 18.187.7.142
248 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
249 --dport 10003 -j ACCEPT
251 Remember to enable iptables!
252 echo 1 > /proc/sys/net/ipv4/ip_forward
254 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
256 token = controls.vnctoken(machine)
257 host = controls.listHost(machine)
259 port = 10003 + [h.hostname for h in config.hosts].index(host)
263 status = controls.statusInfo(machine)
264 has_vnc = hasVnc(status)
266 d = dict(user=username,
270 hostname=state.environ.get('SERVER_NAME', 'localhost'),
273 return templates.vnc(searchList=[d])
275 def getHostname(nic):
276 """Find the hostname associated with a NIC.
278 XXX this should be merged with the similar logic in DNS and DHCP.
281 hostname = nic.hostname
283 hostname = nic.machine.name
289 return hostname + '.' + config.dns.domains[0]
291 def getNicInfo(data_dict, machine):
292 """Helper function for info, get data on nics for a machine.
294 Modifies data_dict to include the relevant data, and returns a list
295 of (key, name) pairs to display "name: data_dict[key]" to the user.
297 data_dict['num_nics'] = len(machine.nics)
298 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
299 ('nic%s_mac', 'NIC %s MAC Addr'),
300 ('nic%s_ip', 'NIC %s IP'),
303 for i in range(len(machine.nics)):
304 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
305 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
306 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
307 data_dict['nic%s_ip' % i] = machine.nics[i].ip
308 if len(machine.nics) == 1:
309 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
312 def getDiskInfo(data_dict, machine):
313 """Helper function for info, get data on disks for a machine.
315 Modifies data_dict to include the relevant data, and returns a list
316 of (key, name) pairs to display "name: data_dict[key]" to the user.
318 data_dict['num_disks'] = len(machine.disks)
319 disk_fields_template = [('%s_size', '%s size')]
321 for disk in machine.disks:
322 name = disk.guest_device_name
323 disk_fields.extend([(x % name, y % name) for x, y in
324 disk_fields_template])
325 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
328 def command(username, state, path, fields):
329 """Handler for running commands like boot and delete on a VM."""
330 back = fields.getfirst('back')
332 d = controls.commandResult(username, state, fields)
333 if d['command'] == 'Delete VM':
335 except InvalidInput, err:
338 print >> sys.stderr, err
343 return templates.command(searchList=[d])
345 state.clear() #Changed global state
346 d = getListDict(username, state)
348 return templates.list(searchList=[d])
350 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
351 return ({'Status': '303 See Other',
352 'Location': 'info?machine_id=%d' % machine.machine_id},
353 "You shouldn't see this message.")
355 raise InvalidInput('back', back, 'Not a known back page.')
357 def modifyDict(username, state, fields):
358 """Modify a machine as specified by CGI arguments.
360 Return a list of local variables for modify.tmpl.
365 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
366 validate = validation.Validate(username, state, **kws)
367 machine = validate.machine
368 oldname = machine.name
370 if hasattr(validate, 'memory'):
371 machine.memory = validate.memory
373 if hasattr(validate, 'vmtype'):
374 machine.type = validate.vmtype
376 if hasattr(validate, 'disksize'):
377 disksize = validate.disksize
378 disk = machine.disks[0]
379 if disk.size != disksize:
380 olddisk[disk.guest_device_name] = disksize
382 session.save_or_update(disk)
385 if hasattr(validate, 'owner') and validate.owner != machine.owner:
386 machine.owner = validate.owner
388 if hasattr(validate, 'name'):
389 machine.name = validate.name
390 for n in machine.nics:
391 if n.hostname == oldname:
392 n.hostname = validate.name
393 if hasattr(validate, 'description'):
394 machine.description = validate.description
395 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
396 machine.administrator = validate.admin
398 if hasattr(validate, 'contact'):
399 machine.contact = validate.contact
401 session.save_or_update(machine)
403 cache_acls.refreshMachine(machine)
408 for diskname in olddisk:
409 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
410 if hasattr(validate, 'name'):
411 controls.renameMachine(machine, oldname, validate.name)
412 return dict(user=username,
416 def modify(username, state, path, fields):
417 """Handler for modifying attributes of a machine."""
419 modify_dict = modifyDict(username, state, fields)
420 except InvalidInput, err:
422 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
424 machine = modify_dict['machine']
427 info_dict = infoDict(username, state, machine)
428 info_dict['err'] = err
430 for field in fields.keys():
431 setattr(info_dict['defaults'], field, fields.getfirst(field))
432 info_dict['result'] = result
433 return templates.info(searchList=[info_dict])
436 def helpHandler(username, state, path, fields):
437 """Handler for help messages."""
438 simple = fields.getfirst('simple')
439 subjects = fields.getlist('subject')
443 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
444 ParaVM. You can access the resulting system by logging into the <a
445 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
446 with your Kerberos tickets; there is no root password so sshd will
449 <p>Under the covers, the autoinstaller uses our own patched version of
450 xen-create-image, which is a tool based on debootstrap. If you log
451 into the serial console while the install is running, you can watch
454 'ParaVM Console': """
455 ParaVM machines do not support local console access over VNC. To
456 access the serial console of these machines, you can SSH with Kerberos
457 to %s, using the name of the machine as your
458 username.""" % config.console.hostname,
460 HVM machines use the virtualization features of the processor, while
461 ParaVM machines rely on a modified kernel to communicate directly with
462 the hypervisor. HVMs support boot CDs of any operating system, and
463 the VNC console applet. The three-minute autoinstaller produces
464 ParaVMs. ParaVMs typically are more efficient, and always support the
465 <a href="help?subject=ParaVM+Console">console server</a>.</p>
467 <p>More details are <a
468 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
469 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
470 (which you can skip by using the autoinstaller to begin with.)</p>
472 <p>We recommend using a ParaVM when possible and an HVM when necessary.
475 Don't ask us! We're as mystified as you are.""",
477 The owner field is used to determine <a
478 href="help?subject=Quotas">quotas</a>. It must be the name of a
479 locker that you are an AFS administrator of. In particular, you or an
480 AFS group you are a member of must have AFS rlidwka bits on the
481 locker. You can check who administers the LOCKER locker using the
482 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
483 href="help?subject=Administrator">administrator</a>.""",
485 The administrator field determines who can access the console and
486 power on and off the machine. This can be either a user or a moira
489 Quotas are determined on a per-locker basis. Each locker may have a
490 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
493 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
494 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
495 your machine will run just fine, but the applet's display of the
496 console will suffer artifacts.
499 <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>
500 <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.
505 subjects = sorted(help_mapping.keys())
507 d = dict(user=username,
510 mapping=help_mapping)
512 return templates.help(searchList=[d])
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(#list=listVms,
646 errortest=throwError)
648 def printHeaders(headers):
649 """Print a dictionary as HTTP headers."""
650 for key, value in headers.iteritems():
651 print '%s: %s' % (key, value)
654 def send_error_mail(subject, body):
657 to = config.web.errormail
663 """ % (to, config.web.hostname, subject, body)
664 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
665 stdin=subprocess.PIPE)
670 def show_error(op, username, fields, err, emsg, traceback):
671 """Print an error page when an exception occurs"""
672 d = dict(op=op, user=username, fields=fields,
673 errorMessage=str(err), stderr=emsg, traceback=traceback)
674 details = templates.error_raw(searchList=[d])
675 exclude = config.web.errormail_exclude
676 if username not in exclude and '*' not in exclude:
677 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
679 d['details'] = details
680 return templates.error(searchList=[d])
682 def handler(username, state, path, fields):
683 operation, path = pathSplit(path)
686 print 'Starting', operation
687 fun = mapping.get(operation, badOperation)
688 return fun(username, state, path, fields)
691 def __init__(self, environ, start_response):
692 self.environ = environ
693 self.start = start_response
695 self.username = getUser(environ)
696 self.state = State(self.username)
697 self.state.environ = environ
702 start_time = time.time()
703 database.clear_cache()
704 sys.stderr = StringIO()
705 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
706 operation = self.environ.get('PATH_INFO', '')
708 self.start("301 Moved Permanently", [('Location', './')])
710 if self.username is None:
714 checkpoint.checkpoint('Before')
715 output = handler(self.username, self.state, operation, fields)
716 checkpoint.checkpoint('After')
718 headers = dict(DEFAULT_HEADERS)
719 if isinstance(output, tuple):
720 new_headers, output = output
721 headers.update(new_headers)
722 e = revertStandardError()
724 if hasattr(output, 'addError'):
727 # This only happens on redirects, so it'd be a pain to get
728 # the message to the user. Maybe in the response is useful.
729 output = output + '\n\nstderr:\n' + e
730 output_string = str(output)
731 checkpoint.checkpoint('output as a string')
732 except Exception, err:
733 if not fields.has_key('js'):
734 if isinstance(err, InvalidInput):
735 self.start('200 OK', [('Content-Type', 'text/html')])
736 e = revertStandardError()
737 yield str(invalidInput(operation, self.username, fields,
741 self.start('500 Internal Server Error',
742 [('Content-Type', 'text/html')])
743 e = revertStandardError()
744 s = show_error(operation, self.username, fields,
745 err, e, traceback.format_exc())
748 status = headers.setdefault('Status', '200 OK')
749 del headers['Status']
750 self.start(status, headers.items())
752 if fields.has_key('timedebug'):
753 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
760 from flup.server.fcgi_fork import WSGIServer
761 WSGIServer(constructor()).run()
763 if __name__ == '__main__':