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 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
45 from invirt.common import InvalidInput, CodeError
48 if path.startswith('/'):
53 return path[:i], path[i:]
57 self.start_time = time.time()
60 def checkpoint(self, s):
61 self.checkpoints.append((s, time.time()))
64 return ('Timing info:\n%s\n' %
65 '\n'.join(['%s: %s' % (d, t - self.start_time) for
66 (d, t) in self.checkpoints]))
68 checkpoint = Checkpoint()
71 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
74 """Return HTML code for a (?) link to a specified help topic"""
75 return ('<span class="helplink"><a href="help?' +
76 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
77 +'" target="_blank" ' +
78 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
80 def makeErrorPre(old, addition):
84 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
86 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
88 Template.database = database
89 Template.config = config
90 Template.helppopup = staticmethod(helppopup)
94 """Class to store a dictionary that will be converted to JSON"""
95 def __init__(self, **kws):
103 return simplejson.dumps(self.data)
105 def addError(self, text):
106 """Add stderr text to be displayed on the website."""
108 makeErrorPre(self.data.get('err'), text)
111 """Class to store default values for fields."""
120 def __init__(self, max_memory=None, max_disk=None, **kws):
121 if max_memory is not None:
122 self.memory = min(self.memory, max_memory)
123 if max_disk is not None:
124 self.max_disk = min(self.disk, max_disk)
126 setattr(self, key, kws[key])
130 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
132 def invalidInput(op, username, fields, err, emsg):
133 """Print an error page when an InvalidInput exception occurs"""
134 d = dict(op=op, user=username, err_field=err.err_field,
135 err_value=str(err.err_value), stderr=emsg,
136 errorMessage=str(err))
137 return templates.invalid(searchList=[d])
140 """Does the machine with a given status list support VNC?"""
144 if l[0] == 'device' and l[1][0] == 'vfb':
146 return 'location' in d
149 def parseCreate(username, state, fields):
150 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
151 validate = validation.Validate(username, state, strict=True, **kws)
152 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
153 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
154 cdrom=getattr(validate, 'cdrom', None),
155 autoinstall=getattr(validate, 'autoinstall', None))
157 def create(username, state, path, fields):
158 """Handler for create requests."""
160 parsed_fields = parseCreate(username, state, fields)
161 machine = controls.createVm(username, state, **parsed_fields)
162 except InvalidInput, err:
166 state.clear() #Changed global state
167 d = getListDict(username, state)
170 for field in fields.keys():
171 setattr(d['defaults'], field, fields.getfirst(field))
173 d['new_machine'] = parsed_fields['name']
174 return templates.list(searchList=[d])
177 def getListDict(username, state):
178 """Gets the list of local variables used by list.tmpl."""
179 checkpoint.checkpoint('Starting')
180 machines = state.machines
181 checkpoint.checkpoint('Got my machines')
184 xmlist = state.xmlist
185 checkpoint.checkpoint('Got uptimes')
186 can_clone = 'ice3' not in state.xmlist_raw
192 m.uptime = xmlist[m]['uptime']
193 if xmlist[m]['console']:
198 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
199 max_memory = validation.maxMemory(username, state)
200 max_disk = validation.maxDisk(username)
201 checkpoint.checkpoint('Got max mem/disk')
202 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 = control.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.
273 if nic.hostname and '.' in nic.hostname:
276 return nic.machine.name + '.' + config.dns.domains[0]
281 def getNicInfo(data_dict, machine):
282 """Helper function for info, get data on nics for a machine.
284 Modifies data_dict to include the relevant data, and returns a list
285 of (key, name) pairs to display "name: data_dict[key]" to the user.
287 data_dict['num_nics'] = len(machine.nics)
288 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
289 ('nic%s_mac', 'NIC %s MAC Addr'),
290 ('nic%s_ip', 'NIC %s IP'),
293 for i in range(len(machine.nics)):
294 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
296 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
297 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
298 data_dict['nic%s_ip' % i] = machine.nics[i].ip
299 if len(machine.nics) == 1:
300 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
303 def getDiskInfo(data_dict, machine):
304 """Helper function for info, get data on disks for a machine.
306 Modifies data_dict to include the relevant data, and returns a list
307 of (key, name) pairs to display "name: data_dict[key]" to the user.
309 data_dict['num_disks'] = len(machine.disks)
310 disk_fields_template = [('%s_size', '%s size')]
312 for disk in machine.disks:
313 name = disk.guest_device_name
314 disk_fields.extend([(x % name, y % name) for x, y in
315 disk_fields_template])
316 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
319 def command(username, state, path, fields):
320 """Handler for running commands like boot and delete on a VM."""
321 back = fields.getfirst('back')
323 d = controls.commandResult(username, state, fields)
324 if d['command'] == 'Delete VM':
326 except InvalidInput, err:
329 print >> sys.stderr, err
334 return templates.command(searchList=[d])
336 state.clear() #Changed global state
337 d = getListDict(username, state)
339 return templates.list(searchList=[d])
341 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
342 return ({'Status': '303 See Other',
343 'Location': 'info?machine_id=%d' % machine.machine_id},
344 "You shouldn't see this message.")
346 raise InvalidInput('back', back, 'Not a known back page.')
348 def modifyDict(username, state, fields):
349 """Modify a machine as specified by CGI arguments.
351 Return a list of local variables for modify.tmpl.
356 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
357 validate = validation.Validate(username, state, **kws)
358 machine = validate.machine
359 oldname = machine.name
361 if hasattr(validate, 'memory'):
362 machine.memory = validate.memory
364 if hasattr(validate, 'vmtype'):
365 machine.type = validate.vmtype
367 if hasattr(validate, 'disksize'):
368 disksize = validate.disksize
369 disk = machine.disks[0]
370 if disk.size != disksize:
371 olddisk[disk.guest_device_name] = disksize
373 session.save_or_update(disk)
376 if hasattr(validate, 'owner') and validate.owner != machine.owner:
377 machine.owner = validate.owner
379 if hasattr(validate, 'name'):
380 machine.name = validate.name
381 if hasattr(validate, 'description'):
382 machine.description = validate.description
383 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
384 machine.administrator = validate.admin
386 if hasattr(validate, 'contact'):
387 machine.contact = validate.contact
389 session.save_or_update(machine)
391 print >> sys.stderr, machine, machine.administrator
392 cache_acls.refreshMachine(machine)
397 for diskname in olddisk:
398 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
399 if hasattr(validate, 'name'):
400 controls.renameMachine(machine, oldname, validate.name)
401 return dict(user=username,
405 def modify(username, state, path, fields):
406 """Handler for modifying attributes of a machine."""
408 modify_dict = modifyDict(username, state, fields)
409 except InvalidInput, err:
411 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
413 machine = modify_dict['machine']
416 info_dict = infoDict(username, state, machine)
417 info_dict['err'] = err
419 for field in fields.keys():
420 setattr(info_dict['defaults'], field, fields.getfirst(field))
421 info_dict['result'] = result
422 return templates.info(searchList=[info_dict])
425 def helpHandler(username, state, path, fields):
426 """Handler for help messages."""
427 simple = fields.getfirst('simple')
428 subjects = fields.getlist('subject')
430 help_mapping = {'ParaVM Console': """
431 ParaVM machines do not support local console access over VNC. To
432 access the serial console of these machines, you can SSH with Kerberos
433 to console.%s, using the name of the machine as your
434 username.""" % config.dns.domains[0],
436 HVM machines use the virtualization features of the processor, while
437 ParaVM machines use Xen's emulation of virtualization features. You
438 want an HVM virtualized machine.""",
440 Don't ask us! We're as mystified as you are.""",
442 The owner field is used to determine <a
443 href="help?subject=Quotas">quotas</a>. It must be the name of a
444 locker that you are an AFS administrator of. In particular, you or an
445 AFS group you are a member of must have AFS rlidwka bits on the
446 locker. You can check who administers the LOCKER locker using the
447 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
448 href="help?subject=Administrator">administrator</a>.""",
450 The administrator field determines who can access the console and
451 power on and off the machine. This can be either a user or a moira
454 Quotas are determined on a per-locker basis. Each locker may have a
455 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
458 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
459 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
460 your machine will run just fine, but the applet's display of the
461 console will suffer artifacts.
464 <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>
465 <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.
470 subjects = sorted(help_mapping.keys())
472 d = dict(user=username,
475 mapping=help_mapping)
477 return templates.help(searchList=[d])
480 def badOperation(u, s, p, e):
481 """Function called when accessing an unknown URI."""
482 return ({'Status': '404 Not Found'}, 'Invalid operation.')
484 def infoDict(username, state, machine):
485 """Get the variables used by info.tmpl."""
486 status = controls.statusInfo(machine)
487 checkpoint.checkpoint('Getting status info')
488 has_vnc = hasVnc(status)
490 main_status = dict(name=machine.name,
491 memory=str(machine.memory))
495 main_status = dict(status[1:])
496 main_status['host'] = controls.listHost(machine)
497 start_time = float(main_status.get('start_time', 0))
498 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
499 cpu_time_float = float(main_status.get('cpu_time', 0))
500 cputime = datetime.timedelta(seconds=int(cpu_time_float))
501 checkpoint.checkpoint('Status')
502 display_fields = """name uptime memory state cpu_weight on_reboot
503 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
504 display_fields = [('name', 'Name'),
505 ('description', 'Description'),
507 ('administrator', 'Administrator'),
508 ('contact', 'Contact'),
511 ('uptime', 'uptime'),
512 ('cputime', 'CPU usage'),
513 ('host', 'Hosted on'),
516 ('state', 'state (xen format)'),
517 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
518 ('on_reboot', 'Action on VM reboot'),
519 ('on_poweroff', 'Action on VM poweroff'),
520 ('on_crash', 'Action on VM crash'),
521 ('on_xend_start', 'Action on Xen start'),
522 ('on_xend_stop', 'Action on Xen stop'),
523 ('bootloader', 'Bootloader options'),
527 machine_info['name'] = machine.name
528 machine_info['description'] = machine.description
529 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
530 machine_info['owner'] = machine.owner
531 machine_info['administrator'] = machine.administrator
532 machine_info['contact'] = machine.contact
534 nic_fields = getNicInfo(machine_info, machine)
535 nic_point = display_fields.index('NIC_INFO')
536 display_fields = (display_fields[:nic_point] + nic_fields +
537 display_fields[nic_point+1:])
539 disk_fields = getDiskInfo(machine_info, machine)
540 disk_point = display_fields.index('DISK_INFO')
541 display_fields = (display_fields[:disk_point] + disk_fields +
542 display_fields[disk_point+1:])
544 main_status['memory'] += ' MiB'
545 for field, disp in display_fields:
546 if field in ('uptime', 'cputime') and locals()[field] is not None:
547 fields.append((disp, locals()[field]))
548 elif field in machine_info:
549 fields.append((disp, machine_info[field]))
550 elif field in main_status:
551 fields.append((disp, main_status[field]))
554 #fields.append((disp, None))
556 checkpoint.checkpoint('Got fields')
559 max_mem = validation.maxMemory(machine.owner, state, machine, False)
560 checkpoint.checkpoint('Got mem')
561 max_disk = validation.maxDisk(machine.owner, machine)
562 defaults = Defaults()
563 for name in 'machine_id name description administrator owner memory contact'.split():
564 setattr(defaults, name, getattr(machine, name))
565 defaults.type = machine.type.type_id
566 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
567 checkpoint.checkpoint('Got defaults')
568 d = dict(user=username,
569 on=status is not None,
577 owner_help=helppopup("Owner"),
581 def info(username, state, path, fields):
582 """Handler for info on a single VM."""
583 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
584 d = infoDict(username, state, machine)
585 checkpoint.checkpoint('Got infodict')
586 return templates.info(searchList=[d])
588 def unauthFront(_, _2, _3, fields):
589 """Information for unauth'd users."""
590 return templates.unauth(searchList=[{'simple' : True}])
592 def admin(username, state, path, fields):
594 return ({'Status': '303 See Other',
595 'Location': 'admin/'},
596 "You shouldn't see this message.")
597 if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
598 raise InvalidInput('username', username,
599 'Not in admin group %s.' % config.web.adminacl)
600 newstate = State(username, isadmin=True)
601 newstate.environ = state.environ
602 return handler(username, newstate, path, fields)
604 def throwError(_, __, ___, ____):
605 """Throw an error, to test the error-tracing mechanisms."""
606 raise RuntimeError("test of the emergency broadcast system")
608 mapping = dict(list=listVms,
618 errortest=throwError)
620 def printHeaders(headers):
621 """Print a dictionary as HTTP headers."""
622 for key, value in headers.iteritems():
623 print '%s: %s' % (key, value)
626 def send_error_mail(subject, body):
629 to = config.web.errormail
635 """ % (to, config.web.hostname, subject, body)
636 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
641 def show_error(op, username, fields, err, emsg, traceback):
642 """Print an error page when an exception occurs"""
643 d = dict(op=op, user=username, fields=fields,
644 errorMessage=str(err), stderr=emsg, traceback=traceback)
645 details = templates.error_raw(searchList=[d])
646 exclude = config.web.errormail_exclude
647 if username not in exclude and '*' not in exclude:
648 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
650 d['details'] = details
651 return templates.error(searchList=[d])
653 def getUser(environ):
654 """Return the current user based on the SSL environment variables"""
655 return environ.get('REMOTE_USER', None)
657 def handler(username, state, path, fields):
658 operation, path = pathSplit(path)
661 print 'Starting', operation
662 fun = mapping.get(operation, badOperation)
663 return fun(username, state, path, fields)
666 def __init__(self, environ, start_response):
667 self.environ = environ
668 self.start = start_response
670 self.username = getUser(environ)
671 self.state = State(self.username)
672 self.state.environ = environ
677 start_time = time.time()
678 database.clear_cache()
679 sys.stderr = StringIO()
680 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
681 operation = self.environ.get('PATH_INFO', '')
683 self.start("301 Moved Permanently", [('Location', './')])
685 if self.username is None:
689 checkpoint.checkpoint('Before')
690 output = handler(self.username, self.state, operation, fields)
691 checkpoint.checkpoint('After')
693 headers = dict(DEFAULT_HEADERS)
694 if isinstance(output, tuple):
695 new_headers, output = output
696 headers.update(new_headers)
697 e = revertStandardError()
699 if hasattr(output, 'addError'):
702 # This only happens on redirects, so it'd be a pain to get
703 # the message to the user. Maybe in the response is useful.
704 output = output + '\n\nstderr:\n' + e
705 output_string = str(output)
706 checkpoint.checkpoint('output as a string')
707 except Exception, err:
708 if not fields.has_key('js'):
709 if isinstance(err, InvalidInput):
710 self.start('200 OK', [('Content-Type', 'text/html')])
711 e = revertStandardError()
712 yield str(invalidInput(operation, self.username, fields,
716 self.start('500 Internal Server Error',
717 [('Content-Type', 'text/html')])
718 e = revertStandardError()
719 s = show_error(operation, self.username, fields,
720 err, e, traceback.format_exc())
723 status = headers.setdefault('Status', '200 OK')
724 del headers['Status']
725 self.start(status, headers.items())
727 if fields.has_key('timedebug'):
728 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
735 from flup.server.fcgi_fork import WSGIServer
736 WSGIServer(constructor()).run()
738 if __name__ == '__main__':