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
46 from invirt.remctl import remctl
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.max_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,
207 checkpoint.checkpoint('Got defaults')
208 def sortkey(machine):
209 return (machine.owner != username, machine.owner, machine.name)
210 machines = sorted(machines, key=sortkey)
211 d = dict(user=username,
212 cant_add_vm=validation.cantAddVm(username, state),
213 max_memory=max_memory,
221 def listVms(username, state, path, fields):
222 """Handler for list requests."""
223 checkpoint.checkpoint('Getting list dict')
224 d = getListDict(username, state)
225 checkpoint.checkpoint('Got list dict')
226 return templates.list(searchList=[d])
228 def vnc(username, state, path, fields):
231 Note that due to same-domain restrictions, the applet connects to
232 the webserver, which needs to forward those requests to the xen
233 server. The Xen server runs another proxy that (1) authenticates
234 and (2) finds the correct port for the VM.
236 You might want iptables like:
238 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
239 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
240 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
241 --dport 10003 -j SNAT --to-source 18.187.7.142
242 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
243 --dport 10003 -j ACCEPT
245 Remember to enable iptables!
246 echo 1 > /proc/sys/net/ipv4/ip_forward
248 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
250 token = controls.remctl('control', machine.name, 'vnctoken').strip()
251 host = controls.listHost(machine)
253 port = 10003 + [h.hostname for h in config.hosts].index(host)
257 status = controls.statusInfo(machine)
258 has_vnc = hasVnc(status)
260 d = dict(user=username,
264 hostname=state.environ.get('SERVER_NAME', 'localhost'),
267 return templates.vnc(searchList=[d])
269 def getHostname(nic):
270 """Find the hostname associated with a NIC.
272 XXX this should be merged with the similar logic in DNS and DHCP.
274 if nic.hostname and '.' in nic.hostname:
277 return nic.machine.name + '.' + config.dns.domains[0]
282 def getNicInfo(data_dict, machine):
283 """Helper function for info, get data on nics for a machine.
285 Modifies data_dict to include the relevant data, and returns a list
286 of (key, name) pairs to display "name: data_dict[key]" to the user.
288 data_dict['num_nics'] = len(machine.nics)
289 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
290 ('nic%s_mac', 'NIC %s MAC Addr'),
291 ('nic%s_ip', 'NIC %s IP'),
294 for i in range(len(machine.nics)):
295 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
297 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
298 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
299 data_dict['nic%s_ip' % i] = machine.nics[i].ip
300 if len(machine.nics) == 1:
301 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
304 def getDiskInfo(data_dict, machine):
305 """Helper function for info, get data on disks for a machine.
307 Modifies data_dict to include the relevant data, and returns a list
308 of (key, name) pairs to display "name: data_dict[key]" to the user.
310 data_dict['num_disks'] = len(machine.disks)
311 disk_fields_template = [('%s_size', '%s size')]
313 for disk in machine.disks:
314 name = disk.guest_device_name
315 disk_fields.extend([(x % name, y % name) for x, y in
316 disk_fields_template])
317 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
320 def command(username, state, path, fields):
321 """Handler for running commands like boot and delete on a VM."""
322 back = fields.getfirst('back')
324 d = controls.commandResult(username, state, fields)
325 if d['command'] == 'Delete VM':
327 except InvalidInput, err:
330 print >> sys.stderr, err
335 return templates.command(searchList=[d])
337 state.clear() #Changed global state
338 d = getListDict(username, state)
340 return templates.list(searchList=[d])
342 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
343 return ({'Status': '303 See Other',
344 'Location': 'info?machine_id=%d' % machine.machine_id},
345 "You shouldn't see this message.")
347 raise InvalidInput('back', back, 'Not a known back page.')
349 def modifyDict(username, state, fields):
350 """Modify a machine as specified by CGI arguments.
352 Return a list of local variables for modify.tmpl.
357 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
358 validate = validation.Validate(username, state, **kws)
359 machine = validate.machine
360 oldname = machine.name
362 if hasattr(validate, 'memory'):
363 machine.memory = validate.memory
365 if hasattr(validate, 'vmtype'):
366 machine.type = validate.vmtype
368 if hasattr(validate, 'disksize'):
369 disksize = validate.disksize
370 disk = machine.disks[0]
371 if disk.size != disksize:
372 olddisk[disk.guest_device_name] = disksize
374 session.save_or_update(disk)
377 if hasattr(validate, 'owner') and validate.owner != machine.owner:
378 machine.owner = validate.owner
380 if hasattr(validate, 'name'):
381 machine.name = validate.name
382 if hasattr(validate, 'description'):
383 machine.description = validate.description
384 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
385 machine.administrator = validate.admin
387 if hasattr(validate, 'contact'):
388 machine.contact = validate.contact
390 session.save_or_update(machine)
392 print >> sys.stderr, machine, machine.administrator
393 cache_acls.refreshMachine(machine)
398 for diskname in olddisk:
399 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
400 if hasattr(validate, 'name'):
401 controls.renameMachine(machine, oldname, validate.name)
402 return dict(user=username,
406 def modify(username, state, path, fields):
407 """Handler for modifying attributes of a machine."""
409 modify_dict = modifyDict(username, state, fields)
410 except InvalidInput, err:
412 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
414 machine = modify_dict['machine']
417 info_dict = infoDict(username, state, machine)
418 info_dict['err'] = err
420 for field in fields.keys():
421 setattr(info_dict['defaults'], field, fields.getfirst(field))
422 info_dict['result'] = result
423 return templates.info(searchList=[info_dict])
426 def helpHandler(username, state, path, fields):
427 """Handler for help messages."""
428 simple = fields.getfirst('simple')
429 subjects = fields.getlist('subject')
431 help_mapping = {'ParaVM Console': """
432 ParaVM machines do not support local console access over VNC. To
433 access the serial console of these machines, you can SSH with Kerberos
434 to console.%s, using the name of the machine as your
435 username.""" % config.dns.domains[0],
437 HVM machines use the virtualization features of the processor, while
438 ParaVM machines use Xen's emulation of virtualization features. You
439 want an HVM virtualized machine.""",
441 Don't ask us! We're as mystified as you are.""",
443 The owner field is used to determine <a
444 href="help?subject=Quotas">quotas</a>. It must be the name of a
445 locker that you are an AFS administrator of. In particular, you or an
446 AFS group you are a member of must have AFS rlidwka bits on the
447 locker. You can check who administers the LOCKER locker using the
448 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
449 href="help?subject=Administrator">administrator</a>.""",
451 The administrator field determines who can access the console and
452 power on and off the machine. This can be either a user or a moira
455 Quotas are determined on a per-locker basis. Each locker may have a
456 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
459 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
460 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
461 your machine will run just fine, but the applet's display of the
462 console will suffer artifacts.
465 <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>
466 <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.
471 subjects = sorted(help_mapping.keys())
473 d = dict(user=username,
476 mapping=help_mapping)
478 return templates.help(searchList=[d])
481 def badOperation(u, s, p, e):
482 """Function called when accessing an unknown URI."""
483 return ({'Status': '404 Not Found'}, 'Invalid operation.')
485 def infoDict(username, state, machine):
486 """Get the variables used by info.tmpl."""
487 status = controls.statusInfo(machine)
488 checkpoint.checkpoint('Getting status info')
489 has_vnc = hasVnc(status)
491 main_status = dict(name=machine.name,
492 memory=str(machine.memory))
496 main_status = dict(status[1:])
497 main_status['host'] = controls.listHost(machine)
498 start_time = float(main_status.get('start_time', 0))
499 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
500 cpu_time_float = float(main_status.get('cpu_time', 0))
501 cputime = datetime.timedelta(seconds=int(cpu_time_float))
502 checkpoint.checkpoint('Status')
503 display_fields = """name uptime memory state cpu_weight on_reboot
504 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
505 display_fields = [('name', 'Name'),
506 ('description', 'Description'),
508 ('administrator', 'Administrator'),
509 ('contact', 'Contact'),
512 ('uptime', 'uptime'),
513 ('cputime', 'CPU usage'),
514 ('host', 'Hosted on'),
517 ('state', 'state (xen format)'),
518 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
519 ('on_reboot', 'Action on VM reboot'),
520 ('on_poweroff', 'Action on VM poweroff'),
521 ('on_crash', 'Action on VM crash'),
522 ('on_xend_start', 'Action on Xen start'),
523 ('on_xend_stop', 'Action on Xen stop'),
524 ('bootloader', 'Bootloader options'),
528 machine_info['name'] = machine.name
529 machine_info['description'] = machine.description
530 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
531 machine_info['owner'] = machine.owner
532 machine_info['administrator'] = machine.administrator
533 machine_info['contact'] = machine.contact
535 nic_fields = getNicInfo(machine_info, machine)
536 nic_point = display_fields.index('NIC_INFO')
537 display_fields = (display_fields[:nic_point] + nic_fields +
538 display_fields[nic_point+1:])
540 disk_fields = getDiskInfo(machine_info, machine)
541 disk_point = display_fields.index('DISK_INFO')
542 display_fields = (display_fields[:disk_point] + disk_fields +
543 display_fields[disk_point+1:])
545 main_status['memory'] += ' MiB'
546 for field, disp in display_fields:
547 if field in ('uptime', 'cputime') and locals()[field] is not None:
548 fields.append((disp, locals()[field]))
549 elif field in machine_info:
550 fields.append((disp, machine_info[field]))
551 elif field in main_status:
552 fields.append((disp, main_status[field]))
555 #fields.append((disp, None))
557 checkpoint.checkpoint('Got fields')
560 max_mem = validation.maxMemory(machine.owner, state, machine, False)
561 checkpoint.checkpoint('Got mem')
562 max_disk = validation.maxDisk(machine.owner, machine)
563 defaults = Defaults()
564 for name in 'machine_id name description administrator owner memory contact'.split():
565 setattr(defaults, name, getattr(machine, name))
566 defaults.type = machine.type.type_id
567 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
568 checkpoint.checkpoint('Got defaults')
569 d = dict(user=username,
570 on=status is not None,
578 owner_help=helppopup("Owner"),
582 def info(username, state, path, fields):
583 """Handler for info on a single VM."""
584 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
585 d = infoDict(username, state, machine)
586 checkpoint.checkpoint('Got infodict')
587 return templates.info(searchList=[d])
589 def unauthFront(_, _2, _3, fields):
590 """Information for unauth'd users."""
591 return templates.unauth(searchList=[{'simple' : True}])
593 def admin(username, state, path, fields):
595 return ({'Status': '303 See Other',
596 'Location': 'admin/'},
597 "You shouldn't see this message.")
598 if not username in getAfsGroupMembers(config.web.adminacl, 'athena.mit.edu'):
599 raise InvalidInput('username', username,
600 'Not in admin group %s.' % config.web.adminacl)
601 newstate = State(username, isadmin=True)
602 newstate.environ = state.environ
603 return handler(username, newstate, path, fields)
605 def throwError(_, __, ___, ____):
606 """Throw an error, to test the error-tracing mechanisms."""
607 raise RuntimeError("test of the emergency broadcast system")
609 mapping = dict(list=listVms,
619 errortest=throwError)
621 def printHeaders(headers):
622 """Print a dictionary as HTTP headers."""
623 for key, value in headers.iteritems():
624 print '%s: %s' % (key, value)
627 def send_error_mail(subject, body):
630 to = config.web.errormail
636 """ % (to, config.web.hostname, subject, body)
637 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
642 def show_error(op, username, fields, err, emsg, traceback):
643 """Print an error page when an exception occurs"""
644 d = dict(op=op, user=username, fields=fields,
645 errorMessage=str(err), stderr=emsg, traceback=traceback)
646 details = templates.error_raw(searchList=[d])
647 exclude = config.web.errormail_exclude
648 if username not in exclude and '*' not in exclude:
649 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
651 d['details'] = details
652 return templates.error(searchList=[d])
654 def getUser(environ):
655 """Return the current user based on the SSL environment variables"""
656 return environ.get('REMOTE_USER', None)
658 def handler(username, state, path, fields):
659 operation, path = pathSplit(path)
662 print 'Starting', operation
663 fun = mapping.get(operation, badOperation)
664 return fun(username, state, path, fields)
667 def __init__(self, environ, start_response):
668 self.environ = environ
669 self.start = start_response
671 self.username = getUser(environ)
672 self.state = State(self.username)
673 self.state.environ = environ
678 start_time = time.time()
679 database.clear_cache()
680 sys.stderr = StringIO()
681 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
682 operation = self.environ.get('PATH_INFO', '')
684 self.start("301 Moved Permanently", [('Location', './')])
686 if self.username is None:
690 checkpoint.checkpoint('Before')
691 output = handler(self.username, self.state, operation, fields)
692 checkpoint.checkpoint('After')
694 headers = dict(DEFAULT_HEADERS)
695 if isinstance(output, tuple):
696 new_headers, output = output
697 headers.update(new_headers)
698 e = revertStandardError()
700 if hasattr(output, 'addError'):
703 # This only happens on redirects, so it'd be a pain to get
704 # the message to the user. Maybe in the response is useful.
705 output = output + '\n\nstderr:\n' + e
706 output_string = str(output)
707 checkpoint.checkpoint('output as a string')
708 except Exception, err:
709 if not fields.has_key('js'):
710 if isinstance(err, InvalidInput):
711 self.start('200 OK', [('Content-Type', 'text/html')])
712 e = revertStandardError()
713 yield str(invalidInput(operation, self.username, fields,
717 self.start('500 Internal Server Error',
718 [('Content-Type', 'text/html')])
719 e = revertStandardError()
720 s = show_error(operation, self.username, fields,
721 err, e, traceback.format_exc())
724 status = headers.setdefault('Status', '200 OK')
725 del headers['Status']
726 self.start(status, headers.items())
728 if fields.has_key('timedebug'):
729 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
736 from flup.server.fcgi_fork import WSGIServer
737 WSGIServer(constructor()).run()
739 if __name__ == '__main__':