2 """Main CGI script for web interface"""
16 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
49 if path.startswith('/'):
54 return path[:i], path[i:]
58 self.start_time = time.time()
61 def checkpoint(self, s):
62 self.checkpoints.append((s, time.time()))
65 return ('Timing info:\n%s\n' %
66 '\n'.join(['%s: %s' % (d, t - self.start_time) for
67 (d, t) in self.checkpoints]))
69 checkpoint = Checkpoint()
72 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
75 """Return HTML code for a (?) link to a specified help topic"""
76 return ('<span class="helplink"><a href="help?' +
77 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
78 +'" target="_blank" ' +
79 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
81 def makeErrorPre(old, addition):
85 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
87 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
89 Template.database = database
90 Template.config = config
91 Template.helppopup = staticmethod(helppopup)
95 """Class to store a dictionary that will be converted to JSON"""
96 def __init__(self, **kws):
104 return simplejson.dumps(self.data)
106 def addError(self, text):
107 """Add stderr text to be displayed on the website."""
109 makeErrorPre(self.data.get('err'), text)
112 """Class to store default values for fields."""
121 def __init__(self, max_memory=None, max_disk=None, **kws):
122 if max_memory is not None:
123 self.memory = min(self.memory, max_memory)
124 if max_disk is not None:
125 self.disk = min(self.disk, max_disk)
127 setattr(self, key, kws[key])
131 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
133 def invalidInput(op, username, fields, err, emsg):
134 """Print an error page when an InvalidInput exception occurs"""
135 d = dict(op=op, user=username, err_field=err.err_field,
136 err_value=str(err.err_value), stderr=emsg,
137 errorMessage=str(err))
138 return templates.invalid(searchList=[d])
141 """Does the machine with a given status list support VNC?"""
145 if l[0] == 'device' and l[1][0] == 'vfb':
147 return 'location' in d
150 def parseCreate(username, state, fields):
151 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
152 validate = validation.Validate(username, state, strict=True, **kws)
153 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
154 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
155 cdrom=getattr(validate, 'cdrom', None),
156 autoinstall=getattr(validate, 'autoinstall', None))
158 def create(username, state, path, fields):
159 """Handler for create requests."""
161 parsed_fields = parseCreate(username, state, fields)
162 machine = controls.createVm(username, state, **parsed_fields)
163 except InvalidInput, err:
167 state.clear() #Changed global state
168 d = getListDict(username, state)
171 for field in fields.keys():
172 setattr(d['defaults'], field, fields.getfirst(field))
174 d['new_machine'] = parsed_fields['name']
175 return templates.list(searchList=[d])
178 def getListDict(username, state):
179 """Gets the list of local variables used by list.tmpl."""
180 checkpoint.checkpoint('Starting')
181 machines = state.machines
182 checkpoint.checkpoint('Got my machines')
185 xmlist = state.xmlist
186 checkpoint.checkpoint('Got uptimes')
187 can_clone = 'ice3' not in state.xmlist_raw
193 m.uptime = xmlist[m]['uptime']
194 if xmlist[m]['console']:
199 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
200 max_memory = validation.maxMemory(username, state)
201 max_disk = validation.maxDisk(username)
202 checkpoint.checkpoint('Got max mem/disk')
203 defaults = Defaults(max_memory=max_memory,
206 checkpoint.checkpoint('Got defaults')
207 def sortkey(machine):
208 return (machine.owner != username, machine.owner, machine.name)
209 machines = sorted(machines, key=sortkey)
210 d = dict(user=username,
211 cant_add_vm=validation.cantAddVm(username, state),
212 max_memory=max_memory,
220 def listVms(username, state, path, fields):
221 """Handler for list requests."""
222 checkpoint.checkpoint('Getting list dict')
223 d = getListDict(username, state)
224 checkpoint.checkpoint('Got list dict')
225 return templates.list(searchList=[d])
227 def vnc(username, state, path, fields):
230 Note that due to same-domain restrictions, the applet connects to
231 the webserver, which needs to forward those requests to the xen
232 server. The Xen server runs another proxy that (1) authenticates
233 and (2) finds the correct port for the VM.
235 You might want iptables like:
237 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
238 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
239 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
240 --dport 10003 -j SNAT --to-source 18.187.7.142
241 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
242 --dport 10003 -j ACCEPT
244 Remember to enable iptables!
245 echo 1 > /proc/sys/net/ipv4/ip_forward
247 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
249 token = controls.vnctoken(machine)
250 host = controls.listHost(machine)
252 port = 10003 + [h.hostname for h in config.hosts].index(host)
256 status = controls.statusInfo(machine)
257 has_vnc = hasVnc(status)
259 d = dict(user=username,
263 hostname=state.environ.get('SERVER_NAME', 'localhost'),
266 return templates.vnc(searchList=[d])
268 def getHostname(nic):
269 """Find the hostname associated with a NIC.
271 XXX this should be merged with the similar logic in DNS and DHCP.
274 hostname = nic.hostname
276 hostname = nic.machine.name
282 return hostname + '.' + config.dns.domains[0]
284 def getNicInfo(data_dict, machine):
285 """Helper function for info, get data on nics for a machine.
287 Modifies data_dict to include the relevant data, and returns a list
288 of (key, name) pairs to display "name: data_dict[key]" to the user.
290 data_dict['num_nics'] = len(machine.nics)
291 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
292 ('nic%s_mac', 'NIC %s MAC Addr'),
293 ('nic%s_ip', 'NIC %s IP'),
296 for i in range(len(machine.nics)):
297 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
298 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
299 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
300 data_dict['nic%s_ip' % i] = machine.nics[i].ip
301 if len(machine.nics) == 1:
302 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
305 def getDiskInfo(data_dict, machine):
306 """Helper function for info, get data on disks for a machine.
308 Modifies data_dict to include the relevant data, and returns a list
309 of (key, name) pairs to display "name: data_dict[key]" to the user.
311 data_dict['num_disks'] = len(machine.disks)
312 disk_fields_template = [('%s_size', '%s size')]
314 for disk in machine.disks:
315 name = disk.guest_device_name
316 disk_fields.extend([(x % name, y % name) for x, y in
317 disk_fields_template])
318 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
321 def command(username, state, path, fields):
322 """Handler for running commands like boot and delete on a VM."""
323 back = fields.getfirst('back')
325 d = controls.commandResult(username, state, fields)
326 if d['command'] == 'Delete VM':
328 except InvalidInput, err:
331 print >> sys.stderr, err
336 return templates.command(searchList=[d])
338 state.clear() #Changed global state
339 d = getListDict(username, state)
341 return templates.list(searchList=[d])
343 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
344 return ({'Status': '303 See Other',
345 'Location': 'info?machine_id=%d' % machine.machine_id},
346 "You shouldn't see this message.")
348 raise InvalidInput('back', back, 'Not a known back page.')
350 def modifyDict(username, state, fields):
351 """Modify a machine as specified by CGI arguments.
353 Return a list of local variables for modify.tmpl.
358 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
359 validate = validation.Validate(username, state, **kws)
360 machine = validate.machine
361 oldname = machine.name
363 if hasattr(validate, 'memory'):
364 machine.memory = validate.memory
366 if hasattr(validate, 'vmtype'):
367 machine.type = validate.vmtype
369 if hasattr(validate, 'disksize'):
370 disksize = validate.disksize
371 disk = machine.disks[0]
372 if disk.size != disksize:
373 olddisk[disk.guest_device_name] = disksize
375 session.save_or_update(disk)
378 if hasattr(validate, 'owner') and validate.owner != machine.owner:
379 machine.owner = validate.owner
381 if hasattr(validate, 'name'):
382 machine.name = validate.name
383 for n in machine.nics:
384 if n.hostname == oldname:
385 n.hostname = validate.name
386 if hasattr(validate, 'description'):
387 machine.description = validate.description
388 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
389 machine.administrator = validate.admin
391 if hasattr(validate, 'contact'):
392 machine.contact = validate.contact
394 session.save_or_update(machine)
396 cache_acls.refreshMachine(machine)
401 for diskname in olddisk:
402 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
403 if hasattr(validate, 'name'):
404 controls.renameMachine(machine, oldname, validate.name)
405 return dict(user=username,
409 def modify(username, state, path, fields):
410 """Handler for modifying attributes of a machine."""
412 modify_dict = modifyDict(username, state, fields)
413 except InvalidInput, err:
415 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
417 machine = modify_dict['machine']
420 info_dict = infoDict(username, state, machine)
421 info_dict['err'] = err
423 for field in fields.keys():
424 setattr(info_dict['defaults'], field, fields.getfirst(field))
425 info_dict['result'] = result
426 return templates.info(searchList=[info_dict])
429 def helpHandler(username, state, path, fields):
430 """Handler for help messages."""
431 simple = fields.getfirst('simple')
432 subjects = fields.getlist('subject')
436 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
437 ParaVM. You can access the resulting system by logging into the <a
438 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
439 with your Kerberos tickets; there is no root password so sshd will
442 <p>Under the covers, the autoinstaller uses our own patched version of
443 xen-create-image, which is a tool based on debootstrap. If you log
444 into the serial console while the install is running, you can watch
447 'ParaVM Console': """
448 ParaVM machines do not support local console access over VNC. To
449 access the serial console of these machines, you can SSH with Kerberos
450 to %s, using the name of the machine as your
451 username.""" % config.console.hostname,
453 HVM machines use the virtualization features of the processor, while
454 ParaVM machines rely on a modified kernel to communicate directly with
455 the hypervisor. HVMs support boot CDs of any operating system, and
456 the VNC console applet. The three-minute autoinstaller produces
457 ParaVMs. ParaVMs typically are more efficient, and always support the
458 <a href="help?subject=ParaVM+Console">console server</a>.</p>
460 <p>More details are <a
461 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
462 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
463 (which you can skip by using the autoinstaller to begin with.)</p>
465 <p>We recommend using a ParaVM when possible and an HVM when necessary.
468 Don't ask us! We're as mystified as you are.""",
470 The owner field is used to determine <a
471 href="help?subject=Quotas">quotas</a>. It must be the name of a
472 locker that you are an AFS administrator of. In particular, you or an
473 AFS group you are a member of must have AFS rlidwka bits on the
474 locker. You can check who administers the LOCKER locker using the
475 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
476 href="help?subject=Administrator">administrator</a>.""",
478 The administrator field determines who can access the console and
479 power on and off the machine. This can be either a user or a moira
482 Quotas are determined on a per-locker basis. Each locker may have a
483 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
486 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
487 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
488 your machine will run just fine, but the applet's display of the
489 console will suffer artifacts.
492 <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>
493 <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.
498 subjects = sorted(help_mapping.keys())
500 d = dict(user=username,
503 mapping=help_mapping)
505 return templates.help(searchList=[d])
508 def badOperation(u, s, p, e):
509 """Function called when accessing an unknown URI."""
510 return ({'Status': '404 Not Found'}, 'Invalid operation.')
512 def infoDict(username, state, machine):
513 """Get the variables used by info.tmpl."""
514 status = controls.statusInfo(machine)
515 checkpoint.checkpoint('Getting status info')
516 has_vnc = hasVnc(status)
518 main_status = dict(name=machine.name,
519 memory=str(machine.memory))
523 main_status = dict(status[1:])
524 main_status['host'] = controls.listHost(machine)
525 start_time = float(main_status.get('start_time', 0))
526 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
527 cpu_time_float = float(main_status.get('cpu_time', 0))
528 cputime = datetime.timedelta(seconds=int(cpu_time_float))
529 checkpoint.checkpoint('Status')
530 display_fields = [('name', 'Name'),
531 ('description', 'Description'),
533 ('administrator', 'Administrator'),
534 ('contact', 'Contact'),
537 ('uptime', 'uptime'),
538 ('cputime', 'CPU usage'),
539 ('host', 'Hosted on'),
542 ('state', 'state (xen format)'),
543 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
547 machine_info['name'] = machine.name
548 machine_info['description'] = machine.description
549 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
550 machine_info['owner'] = machine.owner
551 machine_info['administrator'] = machine.administrator
552 machine_info['contact'] = machine.contact
554 nic_fields = getNicInfo(machine_info, machine)
555 nic_point = display_fields.index('NIC_INFO')
556 display_fields = (display_fields[:nic_point] + nic_fields +
557 display_fields[nic_point+1:])
559 disk_fields = getDiskInfo(machine_info, machine)
560 disk_point = display_fields.index('DISK_INFO')
561 display_fields = (display_fields[:disk_point] + disk_fields +
562 display_fields[disk_point+1:])
564 main_status['memory'] += ' MiB'
565 for field, disp in display_fields:
566 if field in ('uptime', 'cputime') and locals()[field] is not None:
567 fields.append((disp, locals()[field]))
568 elif field in machine_info:
569 fields.append((disp, machine_info[field]))
570 elif field in main_status:
571 fields.append((disp, main_status[field]))
574 #fields.append((disp, None))
576 checkpoint.checkpoint('Got fields')
579 max_mem = validation.maxMemory(machine.owner, state, machine, False)
580 checkpoint.checkpoint('Got mem')
581 max_disk = validation.maxDisk(machine.owner, machine)
582 defaults = Defaults()
583 for name in 'machine_id name description administrator owner memory contact'.split():
584 setattr(defaults, name, getattr(machine, name))
585 defaults.type = machine.type.type_id
586 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
587 checkpoint.checkpoint('Got defaults')
588 d = dict(user=username,
589 on=status is not None,
597 owner_help=helppopup("Owner"),
601 def info(username, state, path, fields):
602 """Handler for info on a single VM."""
603 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
604 d = infoDict(username, state, machine)
605 checkpoint.checkpoint('Got infodict')
606 return templates.info(searchList=[d])
608 def unauthFront(_, _2, _3, fields):
609 """Information for unauth'd users."""
610 return templates.unauth(searchList=[{'simple' : True,
611 'hostname' : socket.getfqdn()}])
613 def admin(username, state, path, fields):
615 return ({'Status': '303 See Other',
616 'Location': 'admin/'},
617 "You shouldn't see this message.")
618 if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
619 raise InvalidInput('username', username,
620 'Not in admin group %s.' % config.web.adminacl)
621 newstate = State(username, isadmin=True)
622 newstate.environ = state.environ
623 return handler(username, newstate, path, fields)
625 def throwError(_, __, ___, ____):
626 """Throw an error, to test the error-tracing mechanisms."""
627 raise RuntimeError("test of the emergency broadcast system")
629 mapping = dict(list=listVms,
639 errortest=throwError)
641 def printHeaders(headers):
642 """Print a dictionary as HTTP headers."""
643 for key, value in headers.iteritems():
644 print '%s: %s' % (key, value)
647 def send_error_mail(subject, body):
650 to = config.web.errormail
656 """ % (to, config.web.hostname, subject, body)
657 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
658 stdin=subprocess.PIPE)
663 def show_error(op, username, fields, err, emsg, traceback):
664 """Print an error page when an exception occurs"""
665 d = dict(op=op, user=username, fields=fields,
666 errorMessage=str(err), stderr=emsg, traceback=traceback)
667 details = templates.error_raw(searchList=[d])
668 exclude = config.web.errormail_exclude
669 if username not in exclude and '*' not in exclude:
670 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
672 d['details'] = details
673 return templates.error(searchList=[d])
675 def getUser(environ):
676 """Return the current user based on the SSL environment variables"""
677 user = environ.get('REMOTE_USER')
681 if environ.get('AUTH_TYPE') == 'Negotiate':
682 # Convert the krb5 principal into a krb4 username
683 if not user.endswith('@%s' % config.kerberos.realm):
686 return user.split('@')[0].replace('/', '.')
690 def handler(username, state, path, fields):
691 operation, path = pathSplit(path)
694 print 'Starting', operation
695 fun = mapping.get(operation, badOperation)
696 return fun(username, state, path, fields)
699 def __init__(self, environ, start_response):
700 self.environ = environ
701 self.start = start_response
703 self.username = getUser(environ)
704 self.state = State(self.username)
705 self.state.environ = environ
710 start_time = time.time()
711 database.clear_cache()
712 sys.stderr = StringIO()
713 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
714 operation = self.environ.get('PATH_INFO', '')
716 self.start("301 Moved Permanently", [('Location', './')])
718 if self.username is None:
722 checkpoint.checkpoint('Before')
723 output = handler(self.username, self.state, operation, fields)
724 checkpoint.checkpoint('After')
726 headers = dict(DEFAULT_HEADERS)
727 if isinstance(output, tuple):
728 new_headers, output = output
729 headers.update(new_headers)
730 e = revertStandardError()
732 if hasattr(output, 'addError'):
735 # This only happens on redirects, so it'd be a pain to get
736 # the message to the user. Maybe in the response is useful.
737 output = output + '\n\nstderr:\n' + e
738 output_string = str(output)
739 checkpoint.checkpoint('output as a string')
740 except Exception, err:
741 if not fields.has_key('js'):
742 if isinstance(err, InvalidInput):
743 self.start('200 OK', [('Content-Type', 'text/html')])
744 e = revertStandardError()
745 yield str(invalidInput(operation, self.username, fields,
749 self.start('500 Internal Server Error',
750 [('Content-Type', 'text/html')])
751 e = revertStandardError()
752 s = show_error(operation, self.username, fields,
753 err, e, traceback.format_exc())
756 status = headers.setdefault('Status', '200 OK')
757 del headers['Status']
758 self.start(status, headers.items())
760 if fields.has_key('timedebug'):
761 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
768 from flup.server.fcgi_fork import WSGIServer
769 WSGIServer(constructor()).run()
771 if __name__ == '__main__':