2 """Main CGI script for web interface"""
15 from StringIO import StringIO
17 def revertStandardError():
18 """Move stderr to stdout, and return the contents of the old stderr."""
20 if not isinstance(errio, StringIO):
22 sys.stderr = sys.stdout
27 """Revert stderr to stdout, and print the contents of stderr"""
28 if isinstance(sys.stderr, StringIO):
29 print revertStandardError()
31 if __name__ == '__main__':
33 atexit.register(printError)
36 from Cheetah.Template import Template
39 from webcommon import InvalidInput, CodeError, State
41 from getafsgroups import getAfsGroupMembers
42 from invirt import database
43 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
44 from invirt.config import structs as config
47 if path.startswith('/'):
52 return path[:i], path[i:]
56 self.start_time = time.time()
59 def checkpoint(self, s):
60 self.checkpoints.append((s, time.time()))
63 return ('Timing info:\n%s\n' %
64 '\n'.join(['%s: %s' % (d, t - self.start_time) for
65 (d, t) in self.checkpoints]))
67 checkpoint = Checkpoint()
70 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
73 """Return HTML code for a (?) link to a specified help topic"""
74 return ('<span class="helplink"><a href="help?' +
75 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
76 +'" target="_blank" ' +
77 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
79 def makeErrorPre(old, addition):
83 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
85 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
87 Template.database = database
88 Template.config = config
89 Template.helppopup = staticmethod(helppopup)
93 """Class to store a dictionary that will be converted to JSON"""
94 def __init__(self, **kws):
102 return simplejson.dumps(self.data)
104 def addError(self, text):
105 """Add stderr text to be displayed on the website."""
107 makeErrorPre(self.data.get('err'), text)
110 """Class to store default values for fields."""
119 def __init__(self, max_memory=None, max_disk=None, **kws):
120 if max_memory is not None:
121 self.memory = min(self.memory, max_memory)
122 if max_disk is not None:
123 self.max_disk = min(self.disk, max_disk)
125 setattr(self, key, kws[key])
129 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
131 def invalidInput(op, username, fields, err, emsg):
132 """Print an error page when an InvalidInput exception occurs"""
133 d = dict(op=op, user=username, err_field=err.err_field,
134 err_value=str(err.err_value), stderr=emsg,
135 errorMessage=str(err))
136 return templates.invalid(searchList=[d])
139 """Does the machine with a given status list support VNC?"""
143 if l[0] == 'device' and l[1][0] == 'vfb':
145 return 'location' in d
148 def parseCreate(username, state, fields):
149 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
150 validate = validation.Validate(username, state, strict=True, **kws)
151 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
152 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
153 cdrom=getattr(validate, 'cdrom', None),
154 autoinstall=getattr(validate, 'autoinstall', None))
156 def create(username, state, path, fields):
157 """Handler for create requests."""
159 parsed_fields = parseCreate(username, state, fields)
160 machine = controls.createVm(username, state, **parsed_fields)
161 except InvalidInput, err:
165 state.clear() #Changed global state
166 d = getListDict(username, state)
169 for field in fields.keys():
170 setattr(d['defaults'], field, fields.getfirst(field))
172 d['new_machine'] = parsed_fields['name']
173 return templates.list(searchList=[d])
176 def getListDict(username, state):
177 """Gets the list of local variables used by list.tmpl."""
178 checkpoint.checkpoint('Starting')
179 machines = state.machines
180 checkpoint.checkpoint('Got my machines')
183 xmlist = state.xmlist
184 checkpoint.checkpoint('Got uptimes')
185 can_clone = 'ice3' not in state.xmlist_raw
191 m.uptime = xmlist[m]['uptime']
192 if xmlist[m]['console']:
197 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
198 max_memory = validation.maxMemory(username, state)
199 max_disk = validation.maxDisk(username)
200 checkpoint.checkpoint('Got max mem/disk')
201 defaults = Defaults(max_memory=max_memory,
205 checkpoint.checkpoint('Got defaults')
206 def sortkey(machine):
207 return (machine.owner != username, machine.owner, machine.name)
208 machines = sorted(machines, key=sortkey)
209 d = dict(user=username,
210 cant_add_vm=validation.cantAddVm(username, state),
211 max_memory=max_memory,
219 def listVms(username, state, path, fields):
220 """Handler for list requests."""
221 checkpoint.checkpoint('Getting list dict')
222 d = getListDict(username, state)
223 checkpoint.checkpoint('Got list dict')
224 return templates.list(searchList=[d])
226 def vnc(username, state, path, fields):
229 Note that due to same-domain restrictions, the applet connects to
230 the webserver, which needs to forward those requests to the xen
231 server. The Xen server runs another proxy that (1) authenticates
232 and (2) finds the correct port for the VM.
234 You might want iptables like:
236 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
237 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
238 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
239 --dport 10003 -j SNAT --to-source 18.187.7.142
240 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
241 --dport 10003 -j ACCEPT
243 Remember to enable iptables!
244 echo 1 > /proc/sys/net/ipv4/ip_forward
246 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
248 token = controls.remctl('control', machine.name, 'vnctoken')
249 host = controls.listHost(machine)
251 port = 10003 + [h.hostname for h in config.hosts].index(host)
255 status = controls.statusInfo(machine)
256 has_vnc = hasVnc(status)
258 d = dict(user=username,
262 hostname=state.environ.get('SERVER_NAME', 'localhost'),
265 return templates.vnc(searchList=[d])
267 def getHostname(nic):
268 """Find the hostname associated with a NIC.
270 XXX this should be merged with the similar logic in DNS and DHCP.
272 if nic.hostname and '.' in nic.hostname:
275 return nic.machine.name + '.' + config.dns.domains[0]
280 def getNicInfo(data_dict, machine):
281 """Helper function for info, get data on nics for a machine.
283 Modifies data_dict to include the relevant data, and returns a list
284 of (key, name) pairs to display "name: data_dict[key]" to the user.
286 data_dict['num_nics'] = len(machine.nics)
287 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
288 ('nic%s_mac', 'NIC %s MAC Addr'),
289 ('nic%s_ip', 'NIC %s IP'),
292 for i in range(len(machine.nics)):
293 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
295 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
296 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
297 data_dict['nic%s_ip' % i] = machine.nics[i].ip
298 if len(machine.nics) == 1:
299 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
302 def getDiskInfo(data_dict, machine):
303 """Helper function for info, get data on disks for a machine.
305 Modifies data_dict to include the relevant data, and returns a list
306 of (key, name) pairs to display "name: data_dict[key]" to the user.
308 data_dict['num_disks'] = len(machine.disks)
309 disk_fields_template = [('%s_size', '%s size')]
311 for disk in machine.disks:
312 name = disk.guest_device_name
313 disk_fields.extend([(x % name, y % name) for x, y in
314 disk_fields_template])
315 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
318 def command(username, state, path, fields):
319 """Handler for running commands like boot and delete on a VM."""
320 back = fields.getfirst('back')
322 d = controls.commandResult(username, state, fields)
323 if d['command'] == 'Delete VM':
325 except InvalidInput, err:
328 print >> sys.stderr, err
333 return templates.command(searchList=[d])
335 state.clear() #Changed global state
336 d = getListDict(username, state)
338 return templates.list(searchList=[d])
340 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
341 return ({'Status': '303 See Other',
342 'Location': 'info?machine_id=%d' % machine.machine_id},
343 "You shouldn't see this message.")
345 raise InvalidInput('back', back, 'Not a known back page.')
347 def modifyDict(username, state, fields):
348 """Modify a machine as specified by CGI arguments.
350 Return a list of local variables for modify.tmpl.
355 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
356 validate = validation.Validate(username, state, **kws)
357 machine = validate.machine
358 oldname = machine.name
360 if hasattr(validate, 'memory'):
361 machine.memory = validate.memory
363 if hasattr(validate, 'vmtype'):
364 machine.type = validate.vmtype
366 if hasattr(validate, 'disksize'):
367 disksize = validate.disksize
368 disk = machine.disks[0]
369 if disk.size != disksize:
370 olddisk[disk.guest_device_name] = disksize
372 session.save_or_update(disk)
375 if hasattr(validate, 'owner') and validate.owner != machine.owner:
376 machine.owner = validate.owner
378 if hasattr(validate, 'name'):
379 machine.name = validate.name
380 if hasattr(validate, 'description'):
381 machine.description = validate.description
382 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
383 machine.administrator = validate.admin
385 if hasattr(validate, 'contact'):
386 machine.contact = validate.contact
388 session.save_or_update(machine)
390 print >> sys.stderr, machine, machine.administrator
391 cache_acls.refreshMachine(machine)
396 for diskname in olddisk:
397 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
398 if hasattr(validate, 'name'):
399 controls.renameMachine(machine, oldname, validate.name)
400 return dict(user=username,
404 def modify(username, state, path, fields):
405 """Handler for modifying attributes of a machine."""
407 modify_dict = modifyDict(username, state, fields)
408 except InvalidInput, err:
410 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
412 machine = modify_dict['machine']
415 info_dict = infoDict(username, state, machine)
416 info_dict['err'] = err
418 for field in fields.keys():
419 setattr(info_dict['defaults'], field, fields.getfirst(field))
420 info_dict['result'] = result
421 return templates.info(searchList=[info_dict])
424 def helpHandler(username, state, path, fields):
425 """Handler for help messages."""
426 simple = fields.getfirst('simple')
427 subjects = fields.getlist('subject')
429 help_mapping = {'ParaVM Console': """
430 ParaVM machines do not support local console access over VNC. To
431 access the serial console of these machines, you can SSH with Kerberos
432 to console.%s, using the name of the machine as your
433 username.""" % config.dns.domains[0],
435 HVM machines use the virtualization features of the processor, while
436 ParaVM machines use Xen's emulation of virtualization features. You
437 want an HVM virtualized machine.""",
439 Don't ask us! We're as mystified as you are.""",
441 The owner field is used to determine <a
442 href="help?subject=Quotas">quotas</a>. It must be the name of a
443 locker that you are an AFS administrator of. In particular, you or an
444 AFS group you are a member of must have AFS rlidwka bits on the
445 locker. You can check who administers the LOCKER locker using the
446 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
447 href="help?subject=Administrator">administrator</a>.""",
449 The administrator field determines who can access the console and
450 power on and off the machine. This can be either a user or a moira
453 Quotas are determined on a per-locker basis. Each locker may have a
454 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
457 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
458 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
459 your machine will run just fine, but the applet's display of the
460 console will suffer artifacts.
463 <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 req uires 512 MB RAM and at least 7.5 GB disk space (15 GB or more recommended).<br>
464 <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.
469 subjects = sorted(help_mapping.keys())
471 d = dict(user=username,
474 mapping=help_mapping)
476 return templates.help(searchList=[d])
479 def badOperation(u, s, p, e):
480 """Function called when accessing an unknown URI."""
481 return ({'Status': '404 Not Found'}, 'Invalid operation.')
483 def infoDict(username, state, machine):
484 """Get the variables used by info.tmpl."""
485 status = controls.statusInfo(machine)
486 checkpoint.checkpoint('Getting status info')
487 has_vnc = hasVnc(status)
489 main_status = dict(name=machine.name,
490 memory=str(machine.memory))
494 main_status = dict(status[1:])
495 main_status['host'] = controls.listHost(machine)
496 start_time = float(main_status.get('start_time', 0))
497 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
498 cpu_time_float = float(main_status.get('cpu_time', 0))
499 cputime = datetime.timedelta(seconds=int(cpu_time_float))
500 checkpoint.checkpoint('Status')
501 display_fields = """name uptime memory state cpu_weight on_reboot
502 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
503 display_fields = [('name', 'Name'),
504 ('description', 'Description'),
506 ('administrator', 'Administrator'),
507 ('contact', 'Contact'),
510 ('uptime', 'uptime'),
511 ('cputime', 'CPU usage'),
512 ('host', 'Hosted on'),
515 ('state', 'state (xen format)'),
516 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
517 ('on_reboot', 'Action on VM reboot'),
518 ('on_poweroff', 'Action on VM poweroff'),
519 ('on_crash', 'Action on VM crash'),
520 ('on_xend_start', 'Action on Xen start'),
521 ('on_xend_stop', 'Action on Xen stop'),
522 ('bootloader', 'Bootloader options'),
526 machine_info['name'] = machine.name
527 machine_info['description'] = machine.description
528 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
529 machine_info['owner'] = machine.owner
530 machine_info['administrator'] = machine.administrator
531 machine_info['contact'] = machine.contact
533 nic_fields = getNicInfo(machine_info, machine)
534 nic_point = display_fields.index('NIC_INFO')
535 display_fields = (display_fields[:nic_point] + nic_fields +
536 display_fields[nic_point+1:])
538 disk_fields = getDiskInfo(machine_info, machine)
539 disk_point = display_fields.index('DISK_INFO')
540 display_fields = (display_fields[:disk_point] + disk_fields +
541 display_fields[disk_point+1:])
543 main_status['memory'] += ' MiB'
544 for field, disp in display_fields:
545 if field in ('uptime', 'cputime') and locals()[field] is not None:
546 fields.append((disp, locals()[field]))
547 elif field in machine_info:
548 fields.append((disp, machine_info[field]))
549 elif field in main_status:
550 fields.append((disp, main_status[field]))
553 #fields.append((disp, None))
555 checkpoint.checkpoint('Got fields')
558 max_mem = validation.maxMemory(machine.owner, state, machine, False)
559 checkpoint.checkpoint('Got mem')
560 max_disk = validation.maxDisk(machine.owner, machine)
561 defaults = Defaults()
562 for name in 'machine_id name description administrator owner memory contact'.split():
563 setattr(defaults, name, getattr(machine, name))
564 defaults.type = machine.type.type_id
565 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
566 checkpoint.checkpoint('Got defaults')
567 d = dict(user=username,
568 on=status is not None,
576 owner_help=helppopup("Owner"),
580 def info(username, state, path, fields):
581 """Handler for info on a single VM."""
582 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
583 d = infoDict(username, state, machine)
584 checkpoint.checkpoint('Got infodict')
585 return templates.info(searchList=[d])
587 def unauthFront(_, _2, _3, fields):
588 """Information for unauth'd users."""
589 return templates.unauth(searchList=[{'simple' : True}])
591 def admin(username, state, path, fields):
593 return ({'Status': '303 See Other',
594 'Location': 'admin/'},
595 "You shouldn't see this message.")
596 if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
597 raise InvalidInput('username', username,
598 'Not in admin group %s.' % config.web.adminacl)
599 newstate = State(username, isadmin=True)
600 newstate.environ = state.environ
601 return handler(username, newstate, path, fields)
603 def throwError(_, __, ___, ____):
604 """Throw an error, to test the error-tracing mechanisms."""
605 raise RuntimeError("test of the emergency broadcast system")
607 mapping = dict(list=listVms,
617 errortest=throwError)
619 def printHeaders(headers):
620 """Print a dictionary as HTTP headers."""
621 for key, value in headers.iteritems():
622 print '%s: %s' % (key, value)
625 def send_error_mail(subject, body):
628 to = config.web.errormail
634 """ % (to, config.web.hostname, subject, body)
635 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
640 def show_error(op, username, fields, err, emsg, traceback):
641 """Print an error page when an exception occurs"""
642 d = dict(op=op, user=username, fields=fields,
643 errorMessage=str(err), stderr=emsg, traceback=traceback)
644 details = templates.error_raw(searchList=[d])
645 exclude = config.web.errormail_exclude
646 if username not in exclude and '*' not in exclude:
647 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
649 d['details'] = details
650 return templates.error(searchList=[d])
652 def getUser(environ):
653 """Return the current user based on the SSL environment variables"""
654 return environ.get('REMOTE_USER', None)
656 def handler(username, state, path, fields):
657 operation, path = pathSplit(path)
660 print 'Starting', operation
661 fun = mapping.get(operation, badOperation)
662 return fun(username, state, path, fields)
665 def __init__(self, environ, start_response):
666 self.environ = environ
667 self.start = start_response
669 self.username = getUser(environ)
670 self.state = State(self.username)
671 self.state.environ = environ
676 start_time = time.time()
677 database.clear_cache()
678 sys.stderr = StringIO()
679 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
680 operation = self.environ.get('PATH_INFO', '')
682 self.start("301 Moved Permanently", [('Location', './')])
684 if self.username is None:
688 checkpoint.checkpoint('Before')
689 output = handler(self.username, self.state, operation, fields)
690 checkpoint.checkpoint('After')
692 headers = dict(DEFAULT_HEADERS)
693 if isinstance(output, tuple):
694 new_headers, output = output
695 headers.update(new_headers)
696 e = revertStandardError()
698 if hasattr(output, 'addError'):
701 # This only happens on redirects, so it'd be a pain to get
702 # the message to the user. Maybe in the response is useful.
703 output = output + '\n\nstderr:\n' + e
704 output_string = str(output)
705 checkpoint.checkpoint('output as a string')
706 except Exception, err:
707 if not fields.has_key('js'):
708 if isinstance(err, InvalidInput):
709 self.start('200 OK', [('Content-Type', 'text/html')])
710 e = revertStandardError()
711 yield str(invalidInput(operation, self.username, fields,
715 self.start('500 Internal Server Error',
716 [('Content-Type', 'text/html')])
717 e = revertStandardError()
718 s = show_error(operation, self.username, fields,
719 err, e, traceback.format_exc())
722 status = headers.setdefault('Status', '200 OK')
723 del headers['Status']
724 self.start(status, headers.items())
726 if fields.has_key('timedebug'):
727 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
734 from flup.server.fcgi_fork import WSGIServer
735 WSGIServer(constructor()).run()
737 if __name__ == '__main__':