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_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
251 data["user"] = username
252 data["machine"] = machine.name
253 data["expires"] = time.time()+(5*60)
254 pickled_data = cPickle.dumps(data)
255 m = hmac.new(TOKEN_KEY, digestmod=sha)
256 m.update(pickled_data)
257 token = {'data': pickled_data, 'digest': m.digest()}
258 token = cPickle.dumps(token)
259 token = base64.urlsafe_b64encode(token)
260 host = controls.listHost(machine)
262 port = 10003 + [h.hostname for h in config.hosts].index(host)
266 status = controls.statusInfo(machine)
267 has_vnc = hasVnc(status)
269 d = dict(user=username,
273 hostname=state.environ.get('SERVER_NAME', 'localhost'),
276 return templates.vnc(searchList=[d])
278 def getHostname(nic):
279 """Find the hostname associated with a NIC.
281 XXX this should be merged with the similar logic in DNS and DHCP.
283 if nic.hostname and '.' in nic.hostname:
286 return nic.machine.name + '.' + 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])
306 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
307 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
308 data_dict['nic%s_ip' % i] = machine.nics[i].ip
309 if len(machine.nics) == 1:
310 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
313 def getDiskInfo(data_dict, machine):
314 """Helper function for info, get data on disks for a machine.
316 Modifies data_dict to include the relevant data, and returns a list
317 of (key, name) pairs to display "name: data_dict[key]" to the user.
319 data_dict['num_disks'] = len(machine.disks)
320 disk_fields_template = [('%s_size', '%s size')]
322 for disk in machine.disks:
323 name = disk.guest_device_name
324 disk_fields.extend([(x % name, y % name) for x, y in
325 disk_fields_template])
326 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
329 def command(username, state, path, fields):
330 """Handler for running commands like boot and delete on a VM."""
331 back = fields.getfirst('back')
333 d = controls.commandResult(username, state, fields)
334 if d['command'] == 'Delete VM':
336 except InvalidInput, err:
339 print >> sys.stderr, err
344 return templates.command(searchList=[d])
346 state.clear() #Changed global state
347 d = getListDict(username, state)
349 return templates.list(searchList=[d])
351 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
352 return ({'Status': '303 See Other',
353 'Location': 'info?machine_id=%d' % machine.machine_id},
354 "You shouldn't see this message.")
356 raise InvalidInput('back', back, 'Not a known back page.')
358 def modifyDict(username, state, fields):
359 """Modify a machine as specified by CGI arguments.
361 Return a list of local variables for modify.tmpl.
366 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
367 validate = validation.Validate(username, state, **kws)
368 machine = validate.machine
369 oldname = machine.name
371 if hasattr(validate, 'memory'):
372 machine.memory = validate.memory
374 if hasattr(validate, 'vmtype'):
375 machine.type = validate.vmtype
377 if hasattr(validate, 'disksize'):
378 disksize = validate.disksize
379 disk = machine.disks[0]
380 if disk.size != disksize:
381 olddisk[disk.guest_device_name] = disksize
383 session.save_or_update(disk)
386 if hasattr(validate, 'owner') and validate.owner != machine.owner:
387 machine.owner = validate.owner
389 if hasattr(validate, 'name'):
390 machine.name = validate.name
391 if hasattr(validate, 'description'):
392 machine.description = validate.description
393 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
394 machine.administrator = validate.admin
396 if hasattr(validate, 'contact'):
397 machine.contact = validate.contact
399 session.save_or_update(machine)
401 print >> sys.stderr, machine, machine.administrator
402 cache_acls.refreshMachine(machine)
407 for diskname in olddisk:
408 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
409 if hasattr(validate, 'name'):
410 controls.renameMachine(machine, oldname, validate.name)
411 return dict(user=username,
415 def modify(username, state, path, fields):
416 """Handler for modifying attributes of a machine."""
418 modify_dict = modifyDict(username, state, fields)
419 except InvalidInput, err:
421 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
423 machine = modify_dict['machine']
426 info_dict = infoDict(username, state, machine)
427 info_dict['err'] = err
429 for field in fields.keys():
430 setattr(info_dict['defaults'], field, fields.getfirst(field))
431 info_dict['result'] = result
432 return templates.info(searchList=[info_dict])
435 def helpHandler(username, state, path, fields):
436 """Handler for help messages."""
437 simple = fields.getfirst('simple')
438 subjects = fields.getlist('subject')
440 help_mapping = {'ParaVM Console': """
441 ParaVM machines do not support local console access over VNC. To
442 access the serial console of these machines, you can SSH with Kerberos
443 to console.%s, using the name of the machine as your
444 username.""" % config.dns.domains[0],
446 HVM machines use the virtualization features of the processor, while
447 ParaVM machines use Xen's emulation of virtualization features. You
448 want an HVM virtualized machine.""",
450 Don't ask us! We're as mystified as you are.""",
452 The owner field is used to determine <a
453 href="help?subject=Quotas">quotas</a>. It must be the name of a
454 locker that you are an AFS administrator of. In particular, you or an
455 AFS group you are a member of must have AFS rlidwka bits on the
456 locker. You can check who administers the LOCKER locker using the
457 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
458 href="help?subject=Administrator">administrator</a>.""",
460 The administrator field determines who can access the console and
461 power on and off the machine. This can be either a user or a moira
464 Quotas are determined on a per-locker basis. Each locker may have a
465 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
468 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
469 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
470 your machine will run just fine, but the applet's display of the
471 console will suffer artifacts.
474 <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>
475 <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.
480 subjects = sorted(help_mapping.keys())
482 d = dict(user=username,
485 mapping=help_mapping)
487 return templates.help(searchList=[d])
490 def badOperation(u, s, p, e):
491 """Function called when accessing an unknown URI."""
492 return ({'Status': '404 Not Found'}, 'Invalid operation.')
494 def infoDict(username, state, machine):
495 """Get the variables used by info.tmpl."""
496 status = controls.statusInfo(machine)
497 checkpoint.checkpoint('Getting status info')
498 has_vnc = hasVnc(status)
500 main_status = dict(name=machine.name,
501 memory=str(machine.memory))
505 main_status = dict(status[1:])
506 main_status['host'] = controls.listHost(machine)
507 start_time = float(main_status.get('start_time', 0))
508 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
509 cpu_time_float = float(main_status.get('cpu_time', 0))
510 cputime = datetime.timedelta(seconds=int(cpu_time_float))
511 checkpoint.checkpoint('Status')
512 display_fields = """name uptime memory state cpu_weight on_reboot
513 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
514 display_fields = [('name', 'Name'),
515 ('description', 'Description'),
517 ('administrator', 'Administrator'),
518 ('contact', 'Contact'),
521 ('uptime', 'uptime'),
522 ('cputime', 'CPU usage'),
523 ('host', 'Hosted on'),
526 ('state', 'state (xen format)'),
527 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
528 ('on_reboot', 'Action on VM reboot'),
529 ('on_poweroff', 'Action on VM poweroff'),
530 ('on_crash', 'Action on VM crash'),
531 ('on_xend_start', 'Action on Xen start'),
532 ('on_xend_stop', 'Action on Xen stop'),
533 ('bootloader', 'Bootloader options'),
537 machine_info['name'] = machine.name
538 machine_info['description'] = machine.description
539 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
540 machine_info['owner'] = machine.owner
541 machine_info['administrator'] = machine.administrator
542 machine_info['contact'] = machine.contact
544 nic_fields = getNicInfo(machine_info, machine)
545 nic_point = display_fields.index('NIC_INFO')
546 display_fields = (display_fields[:nic_point] + nic_fields +
547 display_fields[nic_point+1:])
549 disk_fields = getDiskInfo(machine_info, machine)
550 disk_point = display_fields.index('DISK_INFO')
551 display_fields = (display_fields[:disk_point] + disk_fields +
552 display_fields[disk_point+1:])
554 main_status['memory'] += ' MiB'
555 for field, disp in display_fields:
556 if field in ('uptime', 'cputime') and locals()[field] is not None:
557 fields.append((disp, locals()[field]))
558 elif field in machine_info:
559 fields.append((disp, machine_info[field]))
560 elif field in main_status:
561 fields.append((disp, main_status[field]))
564 #fields.append((disp, None))
566 checkpoint.checkpoint('Got fields')
569 max_mem = validation.maxMemory(machine.owner, state, machine, False)
570 checkpoint.checkpoint('Got mem')
571 max_disk = validation.maxDisk(machine.owner, machine)
572 defaults = Defaults()
573 for name in 'machine_id name description administrator owner memory contact'.split():
574 setattr(defaults, name, getattr(machine, name))
575 defaults.type = machine.type.type_id
576 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
577 checkpoint.checkpoint('Got defaults')
578 d = dict(user=username,
579 on=status is not None,
587 owner_help=helppopup("Owner"),
591 def info(username, state, path, fields):
592 """Handler for info on a single VM."""
593 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
594 d = infoDict(username, state, machine)
595 checkpoint.checkpoint('Got infodict')
596 return templates.info(searchList=[d])
598 def unauthFront(_, _2, _3, fields):
599 """Information for unauth'd users."""
600 return templates.unauth(searchList=[{'simple' : True}])
602 def admin(username, state, path, fields):
604 return ({'Status': '303 See Other',
605 'Location': 'admin/'},
606 "You shouldn't see this message.")
607 if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
608 raise InvalidInput('username', username,
609 'Not in admin group %s.' % config.web.adminacl)
610 newstate = State(username, isadmin=True)
611 newstate.environ = state.environ
612 return handler(username, newstate, path, fields)
614 def throwError(_, __, ___, ____):
615 """Throw an error, to test the error-tracing mechanisms."""
616 raise RuntimeError("test of the emergency broadcast system")
618 mapping = dict(list=listVms,
628 errortest=throwError)
630 def printHeaders(headers):
631 """Print a dictionary as HTTP headers."""
632 for key, value in headers.iteritems():
633 print '%s: %s' % (key, value)
636 def send_error_mail(subject, body):
639 to = config.web.errormail
645 """ % (to, config.web.hostname, subject, body)
646 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
651 def show_error(op, username, fields, err, emsg, traceback):
652 """Print an error page when an exception occurs"""
653 d = dict(op=op, user=username, fields=fields,
654 errorMessage=str(err), stderr=emsg, traceback=traceback)
655 details = templates.error_raw(searchList=[d])
656 if False: #username not in config.web.errormail_exclude:
657 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
659 d['details'] = details
660 return templates.error(searchList=[d])
662 def getUser(environ):
663 """Return the current user based on the SSL environment variables"""
664 return environ.get('REMOTE_USER', None)
666 def handler(username, state, path, fields):
667 operation, path = pathSplit(path)
670 print 'Starting', operation
671 fun = mapping.get(operation, badOperation)
672 return fun(username, state, path, fields)
675 def __init__(self, environ, start_response):
676 self.environ = environ
677 self.start = start_response
679 self.username = getUser(environ)
680 self.state = State(self.username)
681 self.state.environ = environ
686 start_time = time.time()
687 database.clear_cache()
688 sys.stderr = StringIO()
689 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
690 operation = self.environ.get('PATH_INFO', '')
692 self.start("301 Moved Permanently", [('Location', './')])
694 if self.username is None:
698 checkpoint.checkpoint('Before')
699 output = handler(self.username, self.state, operation, fields)
700 checkpoint.checkpoint('After')
702 headers = dict(DEFAULT_HEADERS)
703 if isinstance(output, tuple):
704 new_headers, output = output
705 headers.update(new_headers)
706 e = revertStandardError()
708 if hasattr(output, 'addError'):
711 # This only happens on redirects, so it'd be a pain to get
712 # the message to the user. Maybe in the response is useful.
713 output = output + '\n\nstderr:\n' + e
714 output_string = str(output)
715 checkpoint.checkpoint('output as a string')
716 except Exception, err:
717 if not fields.has_key('js'):
718 if isinstance(err, InvalidInput):
719 self.start('200 OK', [('Content-Type', 'text/html')])
720 e = revertStandardError()
721 yield str(invalidInput(operation, self.username, fields,
725 self.start('500 Internal Server Error',
726 [('Content-Type', 'text/html')])
727 e = revertStandardError()
728 s = show_error(operation, self.username, fields,
729 err, e, traceback.format_exc())
732 status = headers.setdefault('Status', '200 OK')
733 del headers['Status']
734 self.start(status, headers.items())
736 if fields.has_key('timedebug'):
737 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
744 from flup.server.fcgi_fork import WSGIServer
745 WSGIServer(constructor()).run()
747 if __name__ == '__main__':