2 """Main CGI script for web interface"""
17 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
50 class InvirtWeb(View):
52 super(self.__class__,self).__init__()
54 self._cp_config['tools.require_login.on'] = True
55 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
56 'from invirt import database']
60 @cherrypy.tools.mako(filename="/list.mako")
62 """Handler for list requests."""
63 checkpoint.checkpoint('Getting list dict')
64 d = getListDict(cherrypy.request.login, cherrypy.request.state)
65 checkpoint.checkpoint('Got list dict')
70 @cherrypy.tools.mako(filename="/help.mako")
71 def help(self, subject=None, simple=False):
72 """Handler for help messages."""
76 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
77 ParaVM. You can access the resulting system by logging into the <a
78 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
79 with your Kerberos tickets; there is no root password so sshd will
82 <p>Under the covers, the autoinstaller uses our own patched version of
83 xen-create-image, which is a tool based on debootstrap. If you log
84 into the serial console while the install is running, you can watch
88 ParaVM machines do not support local console access over VNC. To
89 access the serial console of these machines, you can SSH with Kerberos
90 to %s, using the name of the machine as your
91 username.""" % config.console.hostname,
93 HVM machines use the virtualization features of the processor, while
94 ParaVM machines rely on a modified kernel to communicate directly with
95 the hypervisor. HVMs support boot CDs of any operating system, and
96 the VNC console applet. The three-minute autoinstaller produces
97 ParaVMs. ParaVMs typically are more efficient, and always support the
98 <a href="help?subject=ParaVM+Console">console server</a>.</p>
100 <p>More details are <a
101 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
102 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
103 (which you can skip by using the autoinstaller to begin with.)</p>
105 <p>We recommend using a ParaVM when possible and an HVM when necessary.
108 Don't ask us! We're as mystified as you are.""",
110 The owner field is used to determine <a
111 href="help?subject=Quotas">quotas</a>. It must be the name of a
112 locker that you are an AFS administrator of. In particular, you or an
113 AFS group you are a member of must have AFS rlidwka bits on the
114 locker. You can check who administers the LOCKER locker using the
115 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
116 href="help?subject=Administrator">administrator</a>.""",
118 The administrator field determines who can access the console and
119 power on and off the machine. This can be either a user or a moira
122 Quotas are determined on a per-locker basis. Each locker may have a
123 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
126 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
127 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
128 your machine will run just fine, but the applet's display of the
129 console will suffer artifacts.
132 <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>
133 <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.
138 subject = sorted(help_mapping.keys())
139 if not isinstance(subject, list):
142 return dict(simple=simple,
144 mapping=help_mapping)
145 help._cp_config['tools.require_login.on'] = False
148 @cherrypy.tools.mako(filename="/helloworld.mako")
149 def helloworld(self, **kwargs):
150 return {'request': cherrypy.request, 'kwargs': kwargs}
151 helloworld._cp_config['tools.require_login.on'] = False
153 class MachineView(View):
154 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
155 # _cp_dispatch, and parse the argument as a list instead of
158 def __getattr__(self, name):
160 machine_id = int(name)
161 cherrypy.request.params['machine_id'] = machine_id
167 @cherrypy.tools.mako(filename="/info.mako")
168 def info(self, machine_id):
169 """Handler for info on a single VM."""
170 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
171 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
172 checkpoint.checkpoint('Got infodict')
176 machine = MachineView()
179 if path.startswith('/'):
184 return path[:i], path[i:]
188 self.start_time = time.time()
189 self.checkpoints = []
191 def checkpoint(self, s):
192 self.checkpoints.append((s, time.time()))
195 return ('Timing info:\n%s\n' %
196 '\n'.join(['%s: %s' % (d, t - self.start_time) for
197 (d, t) in self.checkpoints]))
199 checkpoint = Checkpoint()
201 def makeErrorPre(old, addition):
205 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
207 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
209 Template.database = database
210 Template.config = config
214 """Class to store a dictionary that will be converted to JSON"""
215 def __init__(self, **kws):
223 return simplejson.dumps(self.data)
225 def addError(self, text):
226 """Add stderr text to be displayed on the website."""
228 makeErrorPre(self.data.get('err'), text)
231 """Class to store default values for fields."""
240 def __init__(self, max_memory=None, max_disk=None, **kws):
241 if max_memory is not None:
242 self.memory = min(self.memory, max_memory)
243 if max_disk is not None:
244 self.disk = min(self.disk, max_disk)
246 setattr(self, key, kws[key])
250 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
252 def invalidInput(op, username, fields, err, emsg):
253 """Print an error page when an InvalidInput exception occurs"""
254 d = dict(op=op, user=username, err_field=err.err_field,
255 err_value=str(err.err_value), stderr=emsg,
256 errorMessage=str(err))
257 return templates.invalid(searchList=[d])
260 """Does the machine with a given status list support VNC?"""
264 if l[0] == 'device' and l[1][0] == 'vfb':
266 return 'location' in d
269 def parseCreate(username, state, fields):
270 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
271 validate = validation.Validate(username, state, strict=True, **kws)
272 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
273 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
274 cdrom=getattr(validate, 'cdrom', None),
275 autoinstall=getattr(validate, 'autoinstall', None))
277 def create(username, state, path, fields):
278 """Handler for create requests."""
280 parsed_fields = parseCreate(username, state, fields)
281 machine = controls.createVm(username, state, **parsed_fields)
282 except InvalidInput, err:
286 state.clear() #Changed global state
287 d = getListDict(username, state)
290 for field in fields.keys():
291 setattr(d['defaults'], field, fields.getfirst(field))
293 d['new_machine'] = parsed_fields['name']
294 return templates.list(searchList=[d])
297 def getListDict(username, state):
298 """Gets the list of local variables used by list.tmpl."""
299 checkpoint.checkpoint('Starting')
300 machines = state.machines
301 checkpoint.checkpoint('Got my machines')
304 xmlist = state.xmlist
305 checkpoint.checkpoint('Got uptimes')
306 can_clone = 'ice3' not in state.xmlist_raw
312 m.uptime = xmlist[m]['uptime']
313 if xmlist[m]['console']:
318 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
319 max_memory = validation.maxMemory(username, state)
320 max_disk = validation.maxDisk(username)
321 checkpoint.checkpoint('Got max mem/disk')
322 defaults = Defaults(max_memory=max_memory,
325 checkpoint.checkpoint('Got defaults')
326 def sortkey(machine):
327 return (machine.owner != username, machine.owner, machine.name)
328 machines = sorted(machines, key=sortkey)
329 d = dict(user=username,
330 cant_add_vm=validation.cantAddVm(username, state),
331 max_memory=max_memory,
339 def vnc(username, state, path, fields):
342 Note that due to same-domain restrictions, the applet connects to
343 the webserver, which needs to forward those requests to the xen
344 server. The Xen server runs another proxy that (1) authenticates
345 and (2) finds the correct port for the VM.
347 You might want iptables like:
349 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
350 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
351 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
352 --dport 10003 -j SNAT --to-source 18.187.7.142
353 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
354 --dport 10003 -j ACCEPT
356 Remember to enable iptables!
357 echo 1 > /proc/sys/net/ipv4/ip_forward
359 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
361 token = controls.vnctoken(machine)
362 host = controls.listHost(machine)
364 port = 10003 + [h.hostname for h in config.hosts].index(host)
368 status = controls.statusInfo(machine)
369 has_vnc = hasVnc(status)
371 d = dict(user=username,
375 hostname=state.environ.get('SERVER_NAME', 'localhost'),
378 return templates.vnc(searchList=[d])
380 def getHostname(nic):
381 """Find the hostname associated with a NIC.
383 XXX this should be merged with the similar logic in DNS and DHCP.
386 hostname = nic.hostname
388 hostname = nic.machine.name
394 return hostname + '.' + config.dns.domains[0]
396 def getNicInfo(data_dict, machine):
397 """Helper function for info, get data on nics for a machine.
399 Modifies data_dict to include the relevant data, and returns a list
400 of (key, name) pairs to display "name: data_dict[key]" to the user.
402 data_dict['num_nics'] = len(machine.nics)
403 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
404 ('nic%s_mac', 'NIC %s MAC Addr'),
405 ('nic%s_ip', 'NIC %s IP'),
408 for i in range(len(machine.nics)):
409 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
410 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
411 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
412 data_dict['nic%s_ip' % i] = machine.nics[i].ip
413 if len(machine.nics) == 1:
414 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
417 def getDiskInfo(data_dict, machine):
418 """Helper function for info, get data on disks for a machine.
420 Modifies data_dict to include the relevant data, and returns a list
421 of (key, name) pairs to display "name: data_dict[key]" to the user.
423 data_dict['num_disks'] = len(machine.disks)
424 disk_fields_template = [('%s_size', '%s size')]
426 for disk in machine.disks:
427 name = disk.guest_device_name
428 disk_fields.extend([(x % name, y % name) for x, y in
429 disk_fields_template])
430 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
433 def command(username, state, path, fields):
434 """Handler for running commands like boot and delete on a VM."""
435 back = fields.getfirst('back')
437 d = controls.commandResult(username, state, fields)
438 if d['command'] == 'Delete VM':
440 except InvalidInput, err:
443 print >> sys.stderr, err
448 return templates.command(searchList=[d])
450 state.clear() #Changed global state
451 d = getListDict(username, state)
453 return templates.list(searchList=[d])
455 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
456 return ({'Status': '303 See Other',
457 'Location': 'info?machine_id=%d' % machine.machine_id},
458 "You shouldn't see this message.")
460 raise InvalidInput('back', back, 'Not a known back page.')
462 def modifyDict(username, state, fields):
463 """Modify a machine as specified by CGI arguments.
465 Return a list of local variables for modify.tmpl.
470 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
471 validate = validation.Validate(username, state, **kws)
472 machine = validate.machine
473 oldname = machine.name
475 if hasattr(validate, 'memory'):
476 machine.memory = validate.memory
478 if hasattr(validate, 'vmtype'):
479 machine.type = validate.vmtype
481 if hasattr(validate, 'disksize'):
482 disksize = validate.disksize
483 disk = machine.disks[0]
484 if disk.size != disksize:
485 olddisk[disk.guest_device_name] = disksize
487 session.save_or_update(disk)
490 if hasattr(validate, 'owner') and validate.owner != machine.owner:
491 machine.owner = validate.owner
493 if hasattr(validate, 'name'):
494 machine.name = validate.name
495 for n in machine.nics:
496 if n.hostname == oldname:
497 n.hostname = validate.name
498 if hasattr(validate, 'description'):
499 machine.description = validate.description
500 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
501 machine.administrator = validate.admin
503 if hasattr(validate, 'contact'):
504 machine.contact = validate.contact
506 session.save_or_update(machine)
508 cache_acls.refreshMachine(machine)
513 for diskname in olddisk:
514 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
515 if hasattr(validate, 'name'):
516 controls.renameMachine(machine, oldname, validate.name)
517 return dict(user=username,
521 def modify(username, state, path, fields):
522 """Handler for modifying attributes of a machine."""
524 modify_dict = modifyDict(username, state, fields)
525 except InvalidInput, err:
527 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
529 machine = modify_dict['machine']
532 info_dict = infoDict(username, state, machine)
533 info_dict['err'] = err
535 for field in fields.keys():
536 setattr(info_dict['defaults'], field, fields.getfirst(field))
537 info_dict['result'] = result
538 return templates.info(searchList=[info_dict])
540 def badOperation(u, s, p, e):
541 """Function called when accessing an unknown URI."""
542 return ({'Status': '404 Not Found'}, 'Invalid operation.')
544 def infoDict(username, state, machine):
545 """Get the variables used by info.tmpl."""
546 status = controls.statusInfo(machine)
547 checkpoint.checkpoint('Getting status info')
548 has_vnc = hasVnc(status)
550 main_status = dict(name=machine.name,
551 memory=str(machine.memory))
555 main_status = dict(status[1:])
556 main_status['host'] = controls.listHost(machine)
557 start_time = float(main_status.get('start_time', 0))
558 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
559 cpu_time_float = float(main_status.get('cpu_time', 0))
560 cputime = datetime.timedelta(seconds=int(cpu_time_float))
561 checkpoint.checkpoint('Status')
562 display_fields = [('name', 'Name'),
563 ('description', 'Description'),
565 ('administrator', 'Administrator'),
566 ('contact', 'Contact'),
569 ('uptime', 'uptime'),
570 ('cputime', 'CPU usage'),
571 ('host', 'Hosted on'),
574 ('state', 'state (xen format)'),
575 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
579 machine_info['name'] = machine.name
580 machine_info['description'] = machine.description
581 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
582 machine_info['owner'] = machine.owner
583 machine_info['administrator'] = machine.administrator
584 machine_info['contact'] = machine.contact
586 nic_fields = getNicInfo(machine_info, machine)
587 nic_point = display_fields.index('NIC_INFO')
588 display_fields = (display_fields[:nic_point] + nic_fields +
589 display_fields[nic_point+1:])
591 disk_fields = getDiskInfo(machine_info, machine)
592 disk_point = display_fields.index('DISK_INFO')
593 display_fields = (display_fields[:disk_point] + disk_fields +
594 display_fields[disk_point+1:])
596 main_status['memory'] += ' MiB'
597 for field, disp in display_fields:
598 if field in ('uptime', 'cputime') and locals()[field] is not None:
599 fields.append((disp, locals()[field]))
600 elif field in machine_info:
601 fields.append((disp, machine_info[field]))
602 elif field in main_status:
603 fields.append((disp, main_status[field]))
606 #fields.append((disp, None))
608 checkpoint.checkpoint('Got fields')
611 max_mem = validation.maxMemory(machine.owner, state, machine, False)
612 checkpoint.checkpoint('Got mem')
613 max_disk = validation.maxDisk(machine.owner, machine)
614 defaults = Defaults()
615 for name in 'machine_id name description administrator owner memory contact'.split():
616 setattr(defaults, name, getattr(machine, name))
617 defaults.type = machine.type.type_id
618 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
619 checkpoint.checkpoint('Got defaults')
620 d = dict(user=username,
621 on=status is not None,
629 owner_help=helppopup("Owner"),
633 def unauthFront(_, _2, _3, fields):
634 """Information for unauth'd users."""
635 return templates.unauth(searchList=[{'simple' : True,
636 'hostname' : socket.getfqdn()}])
638 def admin(username, state, path, fields):
640 return ({'Status': '303 See Other',
641 'Location': 'admin/'},
642 "You shouldn't see this message.")
643 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
644 raise InvalidInput('username', username,
645 'Not in admin group %s.' % config.adminacl)
646 newstate = State(username, isadmin=True)
647 newstate.environ = state.environ
648 return handler(username, newstate, path, fields)
650 def throwError(_, __, ___, ____):
651 """Throw an error, to test the error-tracing mechanisms."""
652 raise RuntimeError("test of the emergency broadcast system")
654 mapping = dict(vnc=vnc,
661 errortest=throwError)
663 def printHeaders(headers):
664 """Print a dictionary as HTTP headers."""
665 for key, value in headers.iteritems():
666 print '%s: %s' % (key, value)
669 def send_error_mail(subject, body):
672 to = config.web.errormail
678 """ % (to, config.web.hostname, subject, body)
679 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
680 stdin=subprocess.PIPE)
685 def show_error(op, username, fields, err, emsg, traceback):
686 """Print an error page when an exception occurs"""
687 d = dict(op=op, user=username, fields=fields,
688 errorMessage=str(err), stderr=emsg, traceback=traceback)
689 details = templates.error_raw(searchList=[d])
690 exclude = config.web.errormail_exclude
691 if username not in exclude and '*' not in exclude:
692 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
694 d['details'] = details
695 return templates.error(searchList=[d])
697 def handler(username, state, path, fields):
698 operation, path = pathSplit(path)
701 print 'Starting', operation
702 fun = mapping.get(operation, badOperation)
703 return fun(username, state, path, fields)
706 def __init__(self, environ, start_response):
707 self.environ = environ
708 self.start = start_response
710 self.username = getUser(environ)
711 self.state = State(self.username)
712 self.state.environ = environ
717 start_time = time.time()
718 database.clear_cache()
719 sys.stderr = StringIO()
720 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
721 operation = self.environ.get('PATH_INFO', '')
723 self.start("301 Moved Permanently", [('Location', './')])
725 if self.username is None:
729 checkpoint.checkpoint('Before')
730 output = handler(self.username, self.state, operation, fields)
731 checkpoint.checkpoint('After')
733 headers = dict(DEFAULT_HEADERS)
734 if isinstance(output, tuple):
735 new_headers, output = output
736 headers.update(new_headers)
737 e = revertStandardError()
739 if hasattr(output, 'addError'):
742 # This only happens on redirects, so it'd be a pain to get
743 # the message to the user. Maybe in the response is useful.
744 output = output + '\n\nstderr:\n' + e
745 output_string = str(output)
746 checkpoint.checkpoint('output as a string')
747 except Exception, err:
748 if not fields.has_key('js'):
749 if isinstance(err, InvalidInput):
750 self.start('200 OK', [('Content-Type', 'text/html')])
751 e = revertStandardError()
752 yield str(invalidInput(operation, self.username, fields,
756 self.start('500 Internal Server Error',
757 [('Content-Type', 'text/html')])
758 e = revertStandardError()
759 s = show_error(operation, self.username, fields,
760 err, e, traceback.format_exc())
763 status = headers.setdefault('Status', '200 OK')
764 del headers['Status']
765 self.start(status, headers.items())
767 if fields.has_key('timedebug'):
768 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
775 from flup.server.fcgi_fork import WSGIServer
776 WSGIServer(constructor()).run()
778 if __name__ == '__main__':