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']
58 def __getattr__(self, name):
59 if name in ("admin", "overlord"):
60 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz[0].cell):
61 raise InvalidInput('username', cherrypy.request.login,
62 'Not in admin group %s.' % config.adminacl)
63 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
66 return super(InvirtWeb, self).__getattr__(name)
69 @cherrypy.tools.mako(filename="/list.mako")
70 def list(self, result=None):
71 """Handler for list requests."""
72 checkpoint.checkpoint('Getting list dict')
73 d = getListDict(cherrypy.request.login, cherrypy.request.state)
74 if result is not None:
76 checkpoint.checkpoint('Got list dict')
81 @cherrypy.tools.mako(filename="/help.mako")
82 def help(self, subject=None, simple=False):
83 """Handler for help messages."""
87 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
88 ParaVM. You can access the resulting system by logging into the <a
89 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
90 with your Kerberos tickets; there is no root password so sshd will
93 <p>Under the covers, the autoinstaller uses our own patched version of
94 xen-create-image, which is a tool based on debootstrap. If you log
95 into the serial console while the install is running, you can watch
99 ParaVM machines do not support local console access over VNC. To
100 access the serial console of these machines, you can SSH with Kerberos
101 to %s, using the name of the machine as your
102 username.""" % config.console.hostname,
104 HVM machines use the virtualization features of the processor, while
105 ParaVM machines rely on a modified kernel to communicate directly with
106 the hypervisor. HVMs support boot CDs of any operating system, and
107 the VNC console applet. The three-minute autoinstaller produces
108 ParaVMs. ParaVMs typically are more efficient, and always support the
109 <a href="help?subject=ParaVM+Console">console server</a>.</p>
111 <p>More details are <a
112 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
113 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
114 (which you can skip by using the autoinstaller to begin with.)</p>
116 <p>We recommend using a ParaVM when possible and an HVM when necessary.
119 Don't ask us! We're as mystified as you are.""",
121 The owner field is used to determine <a
122 href="help?subject=Quotas">quotas</a>. It must be the name of a
123 locker that you are an AFS administrator of. In particular, you or an
124 AFS group you are a member of must have AFS rlidwka bits on the
125 locker. You can check who administers the LOCKER locker using the
126 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
127 href="help?subject=Administrator">administrator</a>.""",
129 The administrator field determines who can access the console and
130 power on and off the machine. This can be either a user or a moira
133 Quotas are determined on a per-locker basis. Each locker may have a
134 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
137 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
138 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
139 your machine will run just fine, but the applet's display of the
140 console will suffer artifacts.
143 <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>
144 <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, or visit <a href="http://msca.mit.edu/">http://msca.mit.edu/</a> if you are staff/faculty to request one.
149 subject = sorted(help_mapping.keys())
150 if not isinstance(subject, list):
153 return dict(simple=simple,
155 mapping=help_mapping)
156 help._cp_config['tools.require_login.on'] = False
158 def parseCreate(self, fields):
159 kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
160 validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
161 return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
162 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
163 cdrom=getattr(validate, 'cdrom', None),
164 autoinstall=getattr(validate, 'autoinstall', None))
167 @cherrypy.tools.mako(filename="/list.mako")
168 @cherrypy.tools.require_POST()
169 def create(self, **fields):
170 """Handler for create requests."""
172 parsed_fields = self.parseCreate(fields)
173 machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
174 except InvalidInput, err:
178 cherrypy.request.state.clear() #Changed global state
179 d = getListDict(cherrypy.request.login, cherrypy.request.state)
182 for field in fields.keys():
183 setattr(d['defaults'], field, fields.get(field))
185 d['new_machine'] = parsed_fields['name']
189 @cherrypy.tools.mako(filename="/helloworld.mako")
190 def helloworld(self, **kwargs):
191 return {'request': cherrypy.request, 'kwargs': kwargs}
192 helloworld._cp_config['tools.require_login.on'] = False
196 """Throw an error, to test the error-tracing mechanisms."""
197 raise RuntimeError("test of the emergency broadcast system")
199 class MachineView(View):
200 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
201 # _cp_dispatch, and parse the argument as a list instead of
204 def __getattr__(self, name):
206 machine_id = int(name)
207 cherrypy.request.params['machine_id'] = machine_id
213 @cherrypy.tools.mako(filename="/info.mako")
214 def info(self, machine_id):
215 """Handler for info on a single VM."""
216 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
217 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
218 checkpoint.checkpoint('Got infodict')
223 @cherrypy.tools.mako(filename="/info.mako")
224 @cherrypy.tools.require_POST()
225 def modify(self, machine_id, **fields):
226 """Handler for modifying attributes of a machine."""
228 modify_dict = modifyDict(cherrypy.request.login, cherrypy.request.state, machine_id, fields)
229 except InvalidInput, err:
231 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
233 machine = modify_dict['machine']
236 info_dict = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
237 info_dict['err'] = err
239 for field in fields.keys():
240 setattr(info_dict['defaults'], field, fields.get(field))
241 info_dict['result'] = result
245 @cherrypy.tools.mako(filename="/vnc.mako")
246 def vnc(self, machine_id):
249 Note that due to same-domain restrictions, the applet connects to
250 the webserver, which needs to forward those requests to the xen
251 server. The Xen server runs another proxy that (1) authenticates
252 and (2) finds the correct port for the VM.
254 You might want iptables like:
256 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
257 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
258 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
259 --dport 10003 -j SNAT --to-source 18.187.7.142
260 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
261 --dport 10003 -j ACCEPT
263 Remember to enable iptables!
264 echo 1 > /proc/sys/net/ipv4/ip_forward
266 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
268 token = controls.vnctoken(machine)
269 host = controls.listHost(machine)
271 port = 10003 + [h.hostname for h in config.hosts].index(host)
275 status = controls.statusInfo(machine)
276 has_vnc = hasVnc(status)
281 hostname=cherrypy.request.local.name,
286 @cherrypy.tools.mako(filename="/command.mako")
287 @cherrypy.tools.require_POST()
288 def command(self, command_name, machine_id, **kwargs):
289 """Handler for running commands like boot and delete on a VM."""
290 back = kwargs.get('back', None)
292 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
293 if d['command'] == 'Delete VM':
295 except InvalidInput, err:
298 print >> sys.stderr, err
305 cherrypy.request.state.clear() #Changed global state
306 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
308 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
310 raise InvalidInput('back', back, 'Not a known back page.')
312 machine = MachineView()
315 if path.startswith('/'):
320 return path[:i], path[i:]
324 self.start_time = time.time()
325 self.checkpoints = []
327 def checkpoint(self, s):
328 self.checkpoints.append((s, time.time()))
331 return ('Timing info:\n%s\n' %
332 '\n'.join(['%s: %s' % (d, t - self.start_time) for
333 (d, t) in self.checkpoints]))
335 checkpoint = Checkpoint()
337 def makeErrorPre(old, addition):
341 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
343 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
345 Template.database = database
346 Template.config = config
350 """Class to store a dictionary that will be converted to JSON"""
351 def __init__(self, **kws):
359 return simplejson.dumps(self.data)
361 def addError(self, text):
362 """Add stderr text to be displayed on the website."""
364 makeErrorPre(self.data.get('err'), text)
367 """Class to store default values for fields."""
377 def __init__(self, max_memory=None, max_disk=None, **kws):
378 if max_memory is not None:
379 self.memory = min(self.memory, max_memory)
380 if max_disk is not None:
381 self.disk = min(self.disk, max_disk)
383 setattr(self, key, kws[key])
387 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
389 def invalidInput(op, username, fields, err, emsg):
390 """Print an error page when an InvalidInput exception occurs"""
391 d = dict(op=op, user=username, err_field=err.err_field,
392 err_value=str(err.err_value), stderr=emsg,
393 errorMessage=str(err))
394 return templates.invalid(searchList=[d])
397 """Does the machine with a given status list support VNC?"""
401 if l[0] == 'device' and l[1][0] == 'vfb':
403 return 'location' in d
407 def getListDict(username, state):
408 """Gets the list of local variables used by list.tmpl."""
409 checkpoint.checkpoint('Starting')
410 machines = state.machines
411 checkpoint.checkpoint('Got my machines')
415 xmlist = state.xmlist
416 checkpoint.checkpoint('Got uptimes')
422 m.uptime = xmlist[m]['uptime']
423 if xmlist[m]['console']:
428 has_vnc[m] = "ParaVM"
429 if xmlist[m].get('autoinstall'):
432 installing[m] = False
433 max_memory = validation.maxMemory(username, state)
434 max_disk = validation.maxDisk(username)
435 checkpoint.checkpoint('Got max mem/disk')
436 defaults = Defaults(max_memory=max_memory,
439 checkpoint.checkpoint('Got defaults')
440 def sortkey(machine):
441 return (machine.owner != username, machine.owner, machine.name)
442 machines = sorted(machines, key=sortkey)
443 d = dict(user=username,
444 cant_add_vm=validation.cantAddVm(username, state),
445 max_memory=max_memory,
450 installing=installing)
453 def getHostname(nic):
454 """Find the hostname associated with a NIC.
456 XXX this should be merged with the similar logic in DNS and DHCP.
459 hostname = nic.hostname
461 hostname = nic.machine.name
467 return hostname + '.' + config.dns.domains[0]
469 def getNicInfo(data_dict, machine):
470 """Helper function for info, get data on nics for a machine.
472 Modifies data_dict to include the relevant data, and returns a list
473 of (key, name) pairs to display "name: data_dict[key]" to the user.
475 data_dict['num_nics'] = len(machine.nics)
476 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
477 ('nic%s_mac', 'NIC %s MAC Addr'),
478 ('nic%s_ip', 'NIC %s IP'),
481 for i in range(len(machine.nics)):
482 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
483 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
484 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
485 data_dict['nic%s_ip' % i] = machine.nics[i].ip
486 if len(machine.nics) == 1:
487 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
490 def getDiskInfo(data_dict, machine):
491 """Helper function for info, get data on disks for a machine.
493 Modifies data_dict to include the relevant data, and returns a list
494 of (key, name) pairs to display "name: data_dict[key]" to the user.
496 data_dict['num_disks'] = len(machine.disks)
497 disk_fields_template = [('%s_size', '%s size')]
499 for disk in machine.disks:
500 name = disk.guest_device_name
501 disk_fields.extend([(x % name, y % name) for x, y in
502 disk_fields_template])
503 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
506 def modifyDict(username, state, machine_id, fields):
507 """Modify a machine as specified by CGI arguments.
509 Return a dict containing the machine that was modified.
514 kws = dict([(kw, fields.get(kw)) for kw in 'owner admin contact name description memory vmtype disksize'.split() if fields.get(kw)])
515 kws['machine_id'] = machine_id
516 validate = validation.Validate(username, state, **kws)
517 machine = validate.machine
518 oldname = machine.name
520 if hasattr(validate, 'memory'):
521 machine.memory = validate.memory
523 if hasattr(validate, 'vmtype'):
524 machine.type = validate.vmtype
526 if hasattr(validate, 'disksize'):
527 disksize = validate.disksize
528 disk = machine.disks[0]
529 if disk.size != disksize:
530 olddisk[disk.guest_device_name] = disksize
532 session.save_or_update(disk)
535 if hasattr(validate, 'owner') and validate.owner != machine.owner:
536 machine.owner = validate.owner
538 if hasattr(validate, 'name'):
539 machine.name = validate.name
540 for n in machine.nics:
541 if n.hostname == oldname:
542 n.hostname = validate.name
543 if hasattr(validate, 'description'):
544 machine.description = validate.description
545 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
546 machine.administrator = validate.admin
548 if hasattr(validate, 'contact'):
549 machine.contact = validate.contact
551 session.save_or_update(machine)
553 cache_acls.refreshMachine(machine)
558 for diskname in olddisk:
559 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
560 if hasattr(validate, 'name'):
561 controls.renameMachine(machine, oldname, validate.name)
562 return dict(machine=machine)
565 def badOperation(u, s, p, e):
566 """Function called when accessing an unknown URI."""
567 return ({'Status': '404 Not Found'}, 'Invalid operation.')
569 def infoDict(username, state, machine):
570 """Get the variables used by info.tmpl."""
571 status = controls.statusInfo(machine)
572 checkpoint.checkpoint('Getting status info')
573 has_vnc = hasVnc(status)
575 main_status = dict(name=machine.name,
576 memory=str(machine.memory))
580 main_status = dict(status[1:])
581 main_status['host'] = controls.listHost(machine)
582 start_time = float(main_status.get('start_time', 0))
583 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
584 cpu_time_float = float(main_status.get('cpu_time', 0))
585 cputime = datetime.timedelta(seconds=int(cpu_time_float))
586 checkpoint.checkpoint('Status')
587 display_fields = [('name', 'Name'),
588 ('description', 'Description'),
590 ('administrator', 'Administrator'),
591 ('contact', 'Contact'),
594 ('uptime', 'uptime'),
595 ('cputime', 'CPU usage'),
596 ('host', 'Hosted on'),
599 ('state', 'state (xen format)'),
603 machine_info['name'] = machine.name
604 machine_info['description'] = machine.description
605 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
606 machine_info['owner'] = machine.owner
607 machine_info['administrator'] = machine.administrator
608 machine_info['contact'] = machine.contact
610 nic_fields = getNicInfo(machine_info, machine)
611 nic_point = display_fields.index('NIC_INFO')
612 display_fields = (display_fields[:nic_point] + nic_fields +
613 display_fields[nic_point+1:])
615 disk_fields = getDiskInfo(machine_info, machine)
616 disk_point = display_fields.index('DISK_INFO')
617 display_fields = (display_fields[:disk_point] + disk_fields +
618 display_fields[disk_point+1:])
620 main_status['memory'] += ' MiB'
621 for field, disp in display_fields:
622 if field in ('uptime', 'cputime') and locals()[field] is not None:
623 fields.append((disp, locals()[field]))
624 elif field in machine_info:
625 fields.append((disp, machine_info[field]))
626 elif field in main_status:
627 fields.append((disp, main_status[field]))
630 #fields.append((disp, None))
632 checkpoint.checkpoint('Got fields')
635 max_mem = validation.maxMemory(machine.owner, state, machine, False)
636 checkpoint.checkpoint('Got mem')
637 max_disk = validation.maxDisk(machine.owner, machine)
638 defaults = Defaults()
639 for name in 'machine_id name description administrator owner memory contact'.split():
640 if getattr(machine, name):
641 setattr(defaults, name, getattr(machine, name))
642 defaults.type = machine.type.type_id
643 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
644 checkpoint.checkpoint('Got defaults')
645 d = dict(user=username,
646 on=status is not None,
657 def unauthFront(_, _2, _3, fields):
658 """Information for unauth'd users."""
659 return templates.unauth(searchList=[{'simple' : True,
660 'hostname' : socket.getfqdn()}])
665 def printHeaders(headers):
666 """Print a dictionary as HTTP headers."""
667 for key, value in headers.iteritems():
668 print '%s: %s' % (key, value)
671 def send_error_mail(subject, body):
674 to = config.web.errormail
680 """ % (to, config.web.hostname, subject, body)
681 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
682 stdin=subprocess.PIPE)
687 def show_error(op, username, fields, err, emsg, traceback):
688 """Print an error page when an exception occurs"""
689 d = dict(op=op, user=username, fields=fields,
690 errorMessage=str(err), stderr=emsg, traceback=traceback)
691 details = templates.error_raw(searchList=[d])
692 exclude = config.web.errormail_exclude
693 if username not in exclude and '*' not in exclude:
694 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
696 d['details'] = details
697 return templates.error(searchList=[d])
699 def handler(username, state, path, fields):
700 operation, path = pathSplit(path)
703 print 'Starting', operation
704 fun = mapping.get(operation, badOperation)
705 return fun(username, state, path, fields)
708 def __init__(self, environ, start_response):
709 self.environ = environ
710 self.start = start_response
712 self.username = getUser(environ)
713 self.state = State(self.username)
714 self.state.environ = environ
719 start_time = time.time()
720 database.clear_cache()
721 sys.stderr = StringIO()
722 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
723 operation = self.environ.get('PATH_INFO', '')
725 self.start("301 Moved Permanently", [('Location', './')])
727 if self.username is None:
731 checkpoint.checkpoint('Before')
732 output = handler(self.username, self.state, operation, fields)
733 checkpoint.checkpoint('After')
735 headers = dict(DEFAULT_HEADERS)
736 if isinstance(output, tuple):
737 new_headers, output = output
738 headers.update(new_headers)
739 e = revertStandardError()
741 if hasattr(output, 'addError'):
744 # This only happens on redirects, so it'd be a pain to get
745 # the message to the user. Maybe in the response is useful.
746 output = output + '\n\nstderr:\n' + e
747 output_string = str(output)
748 checkpoint.checkpoint('output as a string')
749 except Exception, err:
750 if not fields.has_key('js'):
751 if isinstance(err, InvalidInput):
752 self.start('200 OK', [('Content-Type', 'text/html')])
753 e = revertStandardError()
754 yield str(invalidInput(operation, self.username, fields,
758 self.start('500 Internal Server Error',
759 [('Content-Type', 'text/html')])
760 e = revertStandardError()
761 s = show_error(operation, self.username, fields,
762 err, e, traceback.format_exc())
765 status = headers.setdefault('Status', '200 OK')
766 del headers['Status']
767 self.start(status, headers.items())
769 if fields.has_key('timedebug'):
770 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
777 from flup.server.fcgi_fork import WSGIServer
778 WSGIServer(constructor()).run()
780 if __name__ == '__main__':