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")
71 def helloworld(self, **kwargs):
72 return {'request': cherrypy.request, 'kwargs': kwargs}
73 helloworld._cp_config['tools.require_login.on'] = False
76 if path.startswith('/'):
81 return path[:i], path[i:]
85 self.start_time = time.time()
88 def checkpoint(self, s):
89 self.checkpoints.append((s, time.time()))
92 return ('Timing info:\n%s\n' %
93 '\n'.join(['%s: %s' % (d, t - self.start_time) for
94 (d, t) in self.checkpoints]))
96 checkpoint = Checkpoint()
98 def makeErrorPre(old, addition):
102 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
104 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
106 Template.database = database
107 Template.config = config
111 """Class to store a dictionary that will be converted to JSON"""
112 def __init__(self, **kws):
120 return simplejson.dumps(self.data)
122 def addError(self, text):
123 """Add stderr text to be displayed on the website."""
125 makeErrorPre(self.data.get('err'), text)
128 """Class to store default values for fields."""
137 def __init__(self, max_memory=None, max_disk=None, **kws):
138 if max_memory is not None:
139 self.memory = min(self.memory, max_memory)
140 if max_disk is not None:
141 self.disk = min(self.disk, max_disk)
143 setattr(self, key, kws[key])
147 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
149 def invalidInput(op, username, fields, err, emsg):
150 """Print an error page when an InvalidInput exception occurs"""
151 d = dict(op=op, user=username, err_field=err.err_field,
152 err_value=str(err.err_value), stderr=emsg,
153 errorMessage=str(err))
154 return templates.invalid(searchList=[d])
157 """Does the machine with a given status list support VNC?"""
161 if l[0] == 'device' and l[1][0] == 'vfb':
163 return 'location' in d
166 def parseCreate(username, state, fields):
167 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
168 validate = validation.Validate(username, state, strict=True, **kws)
169 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
170 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
171 cdrom=getattr(validate, 'cdrom', None),
172 autoinstall=getattr(validate, 'autoinstall', None))
174 def create(username, state, path, fields):
175 """Handler for create requests."""
177 parsed_fields = parseCreate(username, state, fields)
178 machine = controls.createVm(username, state, **parsed_fields)
179 except InvalidInput, err:
183 state.clear() #Changed global state
184 d = getListDict(username, state)
187 for field in fields.keys():
188 setattr(d['defaults'], field, fields.getfirst(field))
190 d['new_machine'] = parsed_fields['name']
191 return templates.list(searchList=[d])
194 def getListDict(username, state):
195 """Gets the list of local variables used by list.tmpl."""
196 checkpoint.checkpoint('Starting')
197 machines = state.machines
198 checkpoint.checkpoint('Got my machines')
201 xmlist = state.xmlist
202 checkpoint.checkpoint('Got uptimes')
203 can_clone = 'ice3' not in state.xmlist_raw
209 m.uptime = xmlist[m]['uptime']
210 if xmlist[m]['console']:
215 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
216 max_memory = validation.maxMemory(username, state)
217 max_disk = validation.maxDisk(username)
218 checkpoint.checkpoint('Got max mem/disk')
219 defaults = Defaults(max_memory=max_memory,
222 checkpoint.checkpoint('Got defaults')
223 def sortkey(machine):
224 return (machine.owner != username, machine.owner, machine.name)
225 machines = sorted(machines, key=sortkey)
226 d = dict(user=username,
227 cant_add_vm=validation.cantAddVm(username, state),
228 max_memory=max_memory,
236 def vnc(username, state, path, fields):
239 Note that due to same-domain restrictions, the applet connects to
240 the webserver, which needs to forward those requests to the xen
241 server. The Xen server runs another proxy that (1) authenticates
242 and (2) finds the correct port for the VM.
244 You might want iptables like:
246 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
247 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
248 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
249 --dport 10003 -j SNAT --to-source 18.187.7.142
250 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
251 --dport 10003 -j ACCEPT
253 Remember to enable iptables!
254 echo 1 > /proc/sys/net/ipv4/ip_forward
256 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
258 token = controls.vnctoken(machine)
259 host = controls.listHost(machine)
261 port = 10003 + [h.hostname for h in config.hosts].index(host)
265 status = controls.statusInfo(machine)
266 has_vnc = hasVnc(status)
268 d = dict(user=username,
272 hostname=state.environ.get('SERVER_NAME', 'localhost'),
275 return templates.vnc(searchList=[d])
277 def getHostname(nic):
278 """Find the hostname associated with a NIC.
280 XXX this should be merged with the similar logic in DNS and DHCP.
283 hostname = nic.hostname
285 hostname = nic.machine.name
291 return hostname + '.' + config.dns.domains[0]
293 def getNicInfo(data_dict, machine):
294 """Helper function for info, get data on nics for a machine.
296 Modifies data_dict to include the relevant data, and returns a list
297 of (key, name) pairs to display "name: data_dict[key]" to the user.
299 data_dict['num_nics'] = len(machine.nics)
300 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
301 ('nic%s_mac', 'NIC %s MAC Addr'),
302 ('nic%s_ip', 'NIC %s IP'),
305 for i in range(len(machine.nics)):
306 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
307 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
308 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
309 data_dict['nic%s_ip' % i] = machine.nics[i].ip
310 if len(machine.nics) == 1:
311 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
314 def getDiskInfo(data_dict, machine):
315 """Helper function for info, get data on disks for a machine.
317 Modifies data_dict to include the relevant data, and returns a list
318 of (key, name) pairs to display "name: data_dict[key]" to the user.
320 data_dict['num_disks'] = len(machine.disks)
321 disk_fields_template = [('%s_size', '%s size')]
323 for disk in machine.disks:
324 name = disk.guest_device_name
325 disk_fields.extend([(x % name, y % name) for x, y in
326 disk_fields_template])
327 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
330 def command(username, state, path, fields):
331 """Handler for running commands like boot and delete on a VM."""
332 back = fields.getfirst('back')
334 d = controls.commandResult(username, state, fields)
335 if d['command'] == 'Delete VM':
337 except InvalidInput, err:
340 print >> sys.stderr, err
345 return templates.command(searchList=[d])
347 state.clear() #Changed global state
348 d = getListDict(username, state)
350 return templates.list(searchList=[d])
352 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
353 return ({'Status': '303 See Other',
354 'Location': 'info?machine_id=%d' % machine.machine_id},
355 "You shouldn't see this message.")
357 raise InvalidInput('back', back, 'Not a known back page.')
359 def modifyDict(username, state, fields):
360 """Modify a machine as specified by CGI arguments.
362 Return a list of local variables for modify.tmpl.
367 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
368 validate = validation.Validate(username, state, **kws)
369 machine = validate.machine
370 oldname = machine.name
372 if hasattr(validate, 'memory'):
373 machine.memory = validate.memory
375 if hasattr(validate, 'vmtype'):
376 machine.type = validate.vmtype
378 if hasattr(validate, 'disksize'):
379 disksize = validate.disksize
380 disk = machine.disks[0]
381 if disk.size != disksize:
382 olddisk[disk.guest_device_name] = disksize
384 session.save_or_update(disk)
387 if hasattr(validate, 'owner') and validate.owner != machine.owner:
388 machine.owner = validate.owner
390 if hasattr(validate, 'name'):
391 machine.name = validate.name
392 for n in machine.nics:
393 if n.hostname == oldname:
394 n.hostname = validate.name
395 if hasattr(validate, 'description'):
396 machine.description = validate.description
397 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
398 machine.administrator = validate.admin
400 if hasattr(validate, 'contact'):
401 machine.contact = validate.contact
403 session.save_or_update(machine)
405 cache_acls.refreshMachine(machine)
410 for diskname in olddisk:
411 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
412 if hasattr(validate, 'name'):
413 controls.renameMachine(machine, oldname, validate.name)
414 return dict(user=username,
418 def modify(username, state, path, fields):
419 """Handler for modifying attributes of a machine."""
421 modify_dict = modifyDict(username, state, fields)
422 except InvalidInput, err:
424 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
426 machine = modify_dict['machine']
429 info_dict = infoDict(username, state, machine)
430 info_dict['err'] = err
432 for field in fields.keys():
433 setattr(info_dict['defaults'], field, fields.getfirst(field))
434 info_dict['result'] = result
435 return templates.info(searchList=[info_dict])
438 def helpHandler(username, state, path, fields):
439 """Handler for help messages."""
440 simple = fields.getfirst('simple')
441 subjects = fields.getlist('subject')
445 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
446 ParaVM. You can access the resulting system by logging into the <a
447 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
448 with your Kerberos tickets; there is no root password so sshd will
451 <p>Under the covers, the autoinstaller uses our own patched version of
452 xen-create-image, which is a tool based on debootstrap. If you log
453 into the serial console while the install is running, you can watch
456 'ParaVM Console': """
457 ParaVM machines do not support local console access over VNC. To
458 access the serial console of these machines, you can SSH with Kerberos
459 to %s, using the name of the machine as your
460 username.""" % config.console.hostname,
462 HVM machines use the virtualization features of the processor, while
463 ParaVM machines rely on a modified kernel to communicate directly with
464 the hypervisor. HVMs support boot CDs of any operating system, and
465 the VNC console applet. The three-minute autoinstaller produces
466 ParaVMs. ParaVMs typically are more efficient, and always support the
467 <a href="help?subject=ParaVM+Console">console server</a>.</p>
469 <p>More details are <a
470 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
471 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
472 (which you can skip by using the autoinstaller to begin with.)</p>
474 <p>We recommend using a ParaVM when possible and an HVM when necessary.
477 Don't ask us! We're as mystified as you are.""",
479 The owner field is used to determine <a
480 href="help?subject=Quotas">quotas</a>. It must be the name of a
481 locker that you are an AFS administrator of. In particular, you or an
482 AFS group you are a member of must have AFS rlidwka bits on the
483 locker. You can check who administers the LOCKER locker using the
484 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
485 href="help?subject=Administrator">administrator</a>.""",
487 The administrator field determines who can access the console and
488 power on and off the machine. This can be either a user or a moira
491 Quotas are determined on a per-locker basis. Each locker may have a
492 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
495 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
496 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
497 your machine will run just fine, but the applet's display of the
498 console will suffer artifacts.
501 <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>
502 <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.
507 subjects = sorted(help_mapping.keys())
509 d = dict(user=username,
512 mapping=help_mapping)
514 return templates.help(searchList=[d])
517 def badOperation(u, s, p, e):
518 """Function called when accessing an unknown URI."""
519 return ({'Status': '404 Not Found'}, 'Invalid operation.')
521 def infoDict(username, state, machine):
522 """Get the variables used by info.tmpl."""
523 status = controls.statusInfo(machine)
524 checkpoint.checkpoint('Getting status info')
525 has_vnc = hasVnc(status)
527 main_status = dict(name=machine.name,
528 memory=str(machine.memory))
532 main_status = dict(status[1:])
533 main_status['host'] = controls.listHost(machine)
534 start_time = float(main_status.get('start_time', 0))
535 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
536 cpu_time_float = float(main_status.get('cpu_time', 0))
537 cputime = datetime.timedelta(seconds=int(cpu_time_float))
538 checkpoint.checkpoint('Status')
539 display_fields = [('name', 'Name'),
540 ('description', 'Description'),
542 ('administrator', 'Administrator'),
543 ('contact', 'Contact'),
546 ('uptime', 'uptime'),
547 ('cputime', 'CPU usage'),
548 ('host', 'Hosted on'),
551 ('state', 'state (xen format)'),
552 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
556 machine_info['name'] = machine.name
557 machine_info['description'] = machine.description
558 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
559 machine_info['owner'] = machine.owner
560 machine_info['administrator'] = machine.administrator
561 machine_info['contact'] = machine.contact
563 nic_fields = getNicInfo(machine_info, machine)
564 nic_point = display_fields.index('NIC_INFO')
565 display_fields = (display_fields[:nic_point] + nic_fields +
566 display_fields[nic_point+1:])
568 disk_fields = getDiskInfo(machine_info, machine)
569 disk_point = display_fields.index('DISK_INFO')
570 display_fields = (display_fields[:disk_point] + disk_fields +
571 display_fields[disk_point+1:])
573 main_status['memory'] += ' MiB'
574 for field, disp in display_fields:
575 if field in ('uptime', 'cputime') and locals()[field] is not None:
576 fields.append((disp, locals()[field]))
577 elif field in machine_info:
578 fields.append((disp, machine_info[field]))
579 elif field in main_status:
580 fields.append((disp, main_status[field]))
583 #fields.append((disp, None))
585 checkpoint.checkpoint('Got fields')
588 max_mem = validation.maxMemory(machine.owner, state, machine, False)
589 checkpoint.checkpoint('Got mem')
590 max_disk = validation.maxDisk(machine.owner, machine)
591 defaults = Defaults()
592 for name in 'machine_id name description administrator owner memory contact'.split():
593 setattr(defaults, name, getattr(machine, name))
594 defaults.type = machine.type.type_id
595 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
596 checkpoint.checkpoint('Got defaults')
597 d = dict(user=username,
598 on=status is not None,
606 owner_help=helppopup("Owner"),
610 def info(username, state, path, fields):
611 """Handler for info on a single VM."""
612 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
613 d = infoDict(username, state, machine)
614 checkpoint.checkpoint('Got infodict')
615 return templates.info(searchList=[d])
617 def unauthFront(_, _2, _3, fields):
618 """Information for unauth'd users."""
619 return templates.unauth(searchList=[{'simple' : True,
620 'hostname' : socket.getfqdn()}])
622 def admin(username, state, path, fields):
624 return ({'Status': '303 See Other',
625 'Location': 'admin/'},
626 "You shouldn't see this message.")
627 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
628 raise InvalidInput('username', username,
629 'Not in admin group %s.' % config.adminacl)
630 newstate = State(username, isadmin=True)
631 newstate.environ = state.environ
632 return handler(username, newstate, path, fields)
634 def throwError(_, __, ___, ____):
635 """Throw an error, to test the error-tracing mechanisms."""
636 raise RuntimeError("test of the emergency broadcast system")
638 mapping = dict(#list=listVms,
648 errortest=throwError)
650 def printHeaders(headers):
651 """Print a dictionary as HTTP headers."""
652 for key, value in headers.iteritems():
653 print '%s: %s' % (key, value)
656 def send_error_mail(subject, body):
659 to = config.web.errormail
665 """ % (to, config.web.hostname, subject, body)
666 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
667 stdin=subprocess.PIPE)
672 def show_error(op, username, fields, err, emsg, traceback):
673 """Print an error page when an exception occurs"""
674 d = dict(op=op, user=username, fields=fields,
675 errorMessage=str(err), stderr=emsg, traceback=traceback)
676 details = templates.error_raw(searchList=[d])
677 exclude = config.web.errormail_exclude
678 if username not in exclude and '*' not in exclude:
679 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
681 d['details'] = details
682 return templates.error(searchList=[d])
684 def handler(username, state, path, fields):
685 operation, path = pathSplit(path)
688 print 'Starting', operation
689 fun = mapping.get(operation, badOperation)
690 return fun(username, state, path, fields)
693 def __init__(self, environ, start_response):
694 self.environ = environ
695 self.start = start_response
697 self.username = getUser(environ)
698 self.state = State(self.username)
699 self.state.environ = environ
704 start_time = time.time()
705 database.clear_cache()
706 sys.stderr = StringIO()
707 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
708 operation = self.environ.get('PATH_INFO', '')
710 self.start("301 Moved Permanently", [('Location', './')])
712 if self.username is None:
716 checkpoint.checkpoint('Before')
717 output = handler(self.username, self.state, operation, fields)
718 checkpoint.checkpoint('After')
720 headers = dict(DEFAULT_HEADERS)
721 if isinstance(output, tuple):
722 new_headers, output = output
723 headers.update(new_headers)
724 e = revertStandardError()
726 if hasattr(output, 'addError'):
729 # This only happens on redirects, so it'd be a pain to get
730 # the message to the user. Maybe in the response is useful.
731 output = output + '\n\nstderr:\n' + e
732 output_string = str(output)
733 checkpoint.checkpoint('output as a string')
734 except Exception, err:
735 if not fields.has_key('js'):
736 if isinstance(err, InvalidInput):
737 self.start('200 OK', [('Content-Type', 'text/html')])
738 e = revertStandardError()
739 yield str(invalidInput(operation, self.username, fields,
743 self.start('500 Internal Server Error',
744 [('Content-Type', 'text/html')])
745 e = revertStandardError()
746 s = show_error(operation, self.username, fields,
747 err, e, traceback.format_exc())
750 status = headers.setdefault('Status', '200 OK')
751 del headers['Status']
752 self.start(status, headers.items())
754 if fields.has_key('timedebug'):
755 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
762 from flup.server.fcgi_fork import WSGIServer
763 WSGIServer(constructor()).run()
765 if __name__ == '__main__':