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")
61 def list(self, result=None):
62 """Handler for list requests."""
63 checkpoint.checkpoint('Getting list dict')
64 d = getListDict(cherrypy.request.login, cherrypy.request.state)
65 if result is not None:
67 checkpoint.checkpoint('Got list dict')
72 @cherrypy.tools.mako(filename="/help.mako")
73 def help(self, subject=None, simple=False):
74 """Handler for help messages."""
78 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
79 ParaVM. You can access the resulting system by logging into the <a
80 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
81 with your Kerberos tickets; there is no root password so sshd will
84 <p>Under the covers, the autoinstaller uses our own patched version of
85 xen-create-image, which is a tool based on debootstrap. If you log
86 into the serial console while the install is running, you can watch
90 ParaVM machines do not support local console access over VNC. To
91 access the serial console of these machines, you can SSH with Kerberos
92 to %s, using the name of the machine as your
93 username.""" % config.console.hostname,
95 HVM machines use the virtualization features of the processor, while
96 ParaVM machines rely on a modified kernel to communicate directly with
97 the hypervisor. HVMs support boot CDs of any operating system, and
98 the VNC console applet. The three-minute autoinstaller produces
99 ParaVMs. ParaVMs typically are more efficient, and always support the
100 <a href="help?subject=ParaVM+Console">console server</a>.</p>
102 <p>More details are <a
103 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
104 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
105 (which you can skip by using the autoinstaller to begin with.)</p>
107 <p>We recommend using a ParaVM when possible and an HVM when necessary.
110 Don't ask us! We're as mystified as you are.""",
112 The owner field is used to determine <a
113 href="help?subject=Quotas">quotas</a>. It must be the name of a
114 locker that you are an AFS administrator of. In particular, you or an
115 AFS group you are a member of must have AFS rlidwka bits on the
116 locker. You can check who administers the LOCKER locker using the
117 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
118 href="help?subject=Administrator">administrator</a>.""",
120 The administrator field determines who can access the console and
121 power on and off the machine. This can be either a user or a moira
124 Quotas are determined on a per-locker basis. Each locker may have a
125 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
128 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
129 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
130 your machine will run just fine, but the applet's display of the
131 console will suffer artifacts.
134 <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>
135 <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.
140 subject = sorted(help_mapping.keys())
141 if not isinstance(subject, list):
144 return dict(simple=simple,
146 mapping=help_mapping)
147 help._cp_config['tools.require_login.on'] = False
149 def parseCreate(self, fields):
150 kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
151 validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
152 return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
153 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
154 cdrom=getattr(validate, 'cdrom', None),
155 autoinstall=getattr(validate, 'autoinstall', None))
158 @cherrypy.tools.mako(filename="/list.mako")
159 @cherrypy.tools.require_POST()
160 def create(self, **fields):
161 """Handler for create requests."""
163 parsed_fields = self.parseCreate(fields)
164 machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
165 except InvalidInput, err:
169 cherrypy.request.state.clear() #Changed global state
170 d = getListDict(cherrypy.request.login, cherrypy.request.state)
173 for field in fields.keys():
174 setattr(d['defaults'], field, fields.get(field))
176 d['new_machine'] = parsed_fields['name']
180 @cherrypy.tools.mako(filename="/helloworld.mako")
181 def helloworld(self, **kwargs):
182 return {'request': cherrypy.request, 'kwargs': kwargs}
183 helloworld._cp_config['tools.require_login.on'] = False
185 class MachineView(View):
186 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
187 # _cp_dispatch, and parse the argument as a list instead of
190 def __getattr__(self, name):
192 machine_id = int(name)
193 cherrypy.request.params['machine_id'] = machine_id
199 @cherrypy.tools.mako(filename="/info.mako")
200 def info(self, machine_id):
201 """Handler for info on a single VM."""
202 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
203 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
204 checkpoint.checkpoint('Got infodict')
209 @cherrypy.tools.mako(filename="/vnc.mako")
210 def vnc(self, machine_id):
213 Note that due to same-domain restrictions, the applet connects to
214 the webserver, which needs to forward those requests to the xen
215 server. The Xen server runs another proxy that (1) authenticates
216 and (2) finds the correct port for the VM.
218 You might want iptables like:
220 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
221 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
222 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
223 --dport 10003 -j SNAT --to-source 18.187.7.142
224 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
225 --dport 10003 -j ACCEPT
227 Remember to enable iptables!
228 echo 1 > /proc/sys/net/ipv4/ip_forward
230 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
232 token = controls.vnctoken(machine)
233 host = controls.listHost(machine)
235 port = 10003 + [h.hostname for h in config.hosts].index(host)
239 status = controls.statusInfo(machine)
240 has_vnc = hasVnc(status)
245 hostname=cherrypy.request.local.name,
250 @cherrypy.tools.mako(filename="/command.mako")
251 @cherrypy.tools.require_POST()
252 def command(self, command_name, machine_id, **kwargs):
253 """Handler for running commands like boot and delete on a VM."""
254 back = kwargs.get('back', None)
256 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
257 if d['command'] == 'Delete VM':
259 except InvalidInput, err:
262 print >> sys.stderr, err
269 cherrypy.request.state.clear() #Changed global state
270 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
272 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
274 raise InvalidInput('back', back, 'Not a known back page.')
276 machine = MachineView()
279 if path.startswith('/'):
284 return path[:i], path[i:]
288 self.start_time = time.time()
289 self.checkpoints = []
291 def checkpoint(self, s):
292 self.checkpoints.append((s, time.time()))
295 return ('Timing info:\n%s\n' %
296 '\n'.join(['%s: %s' % (d, t - self.start_time) for
297 (d, t) in self.checkpoints]))
299 checkpoint = Checkpoint()
301 def makeErrorPre(old, addition):
305 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
307 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
309 Template.database = database
310 Template.config = config
314 """Class to store a dictionary that will be converted to JSON"""
315 def __init__(self, **kws):
323 return simplejson.dumps(self.data)
325 def addError(self, text):
326 """Add stderr text to be displayed on the website."""
328 makeErrorPre(self.data.get('err'), text)
331 """Class to store default values for fields."""
340 def __init__(self, max_memory=None, max_disk=None, **kws):
341 if max_memory is not None:
342 self.memory = min(self.memory, max_memory)
343 if max_disk is not None:
344 self.disk = min(self.disk, max_disk)
346 setattr(self, key, kws[key])
350 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
352 def invalidInput(op, username, fields, err, emsg):
353 """Print an error page when an InvalidInput exception occurs"""
354 d = dict(op=op, user=username, err_field=err.err_field,
355 err_value=str(err.err_value), stderr=emsg,
356 errorMessage=str(err))
357 return templates.invalid(searchList=[d])
360 """Does the machine with a given status list support VNC?"""
364 if l[0] == 'device' and l[1][0] == 'vfb':
366 return 'location' in d
370 def getListDict(username, state):
371 """Gets the list of local variables used by list.tmpl."""
372 checkpoint.checkpoint('Starting')
373 machines = state.machines
374 checkpoint.checkpoint('Got my machines')
377 xmlist = state.xmlist
378 checkpoint.checkpoint('Got uptimes')
379 can_clone = 'ice3' not in state.xmlist_raw
385 m.uptime = xmlist[m]['uptime']
386 if xmlist[m]['console']:
391 has_vnc[m] = "ParaVM"
392 max_memory = validation.maxMemory(username, state)
393 max_disk = validation.maxDisk(username)
394 checkpoint.checkpoint('Got max mem/disk')
395 defaults = Defaults(max_memory=max_memory,
398 checkpoint.checkpoint('Got defaults')
399 def sortkey(machine):
400 return (machine.owner != username, machine.owner, machine.name)
401 machines = sorted(machines, key=sortkey)
402 d = dict(user=username,
403 cant_add_vm=validation.cantAddVm(username, state),
404 max_memory=max_memory,
412 def getHostname(nic):
413 """Find the hostname associated with a NIC.
415 XXX this should be merged with the similar logic in DNS and DHCP.
418 hostname = nic.hostname
420 hostname = nic.machine.name
426 return hostname + '.' + config.dns.domains[0]
428 def getNicInfo(data_dict, machine):
429 """Helper function for info, get data on nics for a machine.
431 Modifies data_dict to include the relevant data, and returns a list
432 of (key, name) pairs to display "name: data_dict[key]" to the user.
434 data_dict['num_nics'] = len(machine.nics)
435 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
436 ('nic%s_mac', 'NIC %s MAC Addr'),
437 ('nic%s_ip', 'NIC %s IP'),
440 for i in range(len(machine.nics)):
441 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
442 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
443 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
444 data_dict['nic%s_ip' % i] = machine.nics[i].ip
445 if len(machine.nics) == 1:
446 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
449 def getDiskInfo(data_dict, machine):
450 """Helper function for info, get data on disks for a machine.
452 Modifies data_dict to include the relevant data, and returns a list
453 of (key, name) pairs to display "name: data_dict[key]" to the user.
455 data_dict['num_disks'] = len(machine.disks)
456 disk_fields_template = [('%s_size', '%s size')]
458 for disk in machine.disks:
459 name = disk.guest_device_name
460 disk_fields.extend([(x % name, y % name) for x, y in
461 disk_fields_template])
462 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
465 def modifyDict(username, state, fields):
466 """Modify a machine as specified by CGI arguments.
468 Return a list of local variables for modify.tmpl.
473 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
474 validate = validation.Validate(username, state, **kws)
475 machine = validate.machine
476 oldname = machine.name
478 if hasattr(validate, 'memory'):
479 machine.memory = validate.memory
481 if hasattr(validate, 'vmtype'):
482 machine.type = validate.vmtype
484 if hasattr(validate, 'disksize'):
485 disksize = validate.disksize
486 disk = machine.disks[0]
487 if disk.size != disksize:
488 olddisk[disk.guest_device_name] = disksize
490 session.save_or_update(disk)
493 if hasattr(validate, 'owner') and validate.owner != machine.owner:
494 machine.owner = validate.owner
496 if hasattr(validate, 'name'):
497 machine.name = validate.name
498 for n in machine.nics:
499 if n.hostname == oldname:
500 n.hostname = validate.name
501 if hasattr(validate, 'description'):
502 machine.description = validate.description
503 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
504 machine.administrator = validate.admin
506 if hasattr(validate, 'contact'):
507 machine.contact = validate.contact
509 session.save_or_update(machine)
511 cache_acls.refreshMachine(machine)
516 for diskname in olddisk:
517 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
518 if hasattr(validate, 'name'):
519 controls.renameMachine(machine, oldname, validate.name)
520 return dict(user=username,
524 def modify(username, state, path, fields):
525 """Handler for modifying attributes of a machine."""
527 modify_dict = modifyDict(username, state, fields)
528 except InvalidInput, err:
530 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
532 machine = modify_dict['machine']
535 info_dict = infoDict(username, state, machine)
536 info_dict['err'] = err
538 for field in fields.keys():
539 setattr(info_dict['defaults'], field, fields.getfirst(field))
540 info_dict['result'] = result
541 return templates.info(searchList=[info_dict])
543 def badOperation(u, s, p, e):
544 """Function called when accessing an unknown URI."""
545 return ({'Status': '404 Not Found'}, 'Invalid operation.')
547 def infoDict(username, state, machine):
548 """Get the variables used by info.tmpl."""
549 status = controls.statusInfo(machine)
550 checkpoint.checkpoint('Getting status info')
551 has_vnc = hasVnc(status)
553 main_status = dict(name=machine.name,
554 memory=str(machine.memory))
558 main_status = dict(status[1:])
559 main_status['host'] = controls.listHost(machine)
560 start_time = float(main_status.get('start_time', 0))
561 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
562 cpu_time_float = float(main_status.get('cpu_time', 0))
563 cputime = datetime.timedelta(seconds=int(cpu_time_float))
564 checkpoint.checkpoint('Status')
565 display_fields = [('name', 'Name'),
566 ('description', 'Description'),
568 ('administrator', 'Administrator'),
569 ('contact', 'Contact'),
572 ('uptime', 'uptime'),
573 ('cputime', 'CPU usage'),
574 ('host', 'Hosted on'),
577 ('state', 'state (xen format)'),
581 machine_info['name'] = machine.name
582 machine_info['description'] = machine.description
583 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
584 machine_info['owner'] = machine.owner
585 machine_info['administrator'] = machine.administrator
586 machine_info['contact'] = machine.contact
588 nic_fields = getNicInfo(machine_info, machine)
589 nic_point = display_fields.index('NIC_INFO')
590 display_fields = (display_fields[:nic_point] + nic_fields +
591 display_fields[nic_point+1:])
593 disk_fields = getDiskInfo(machine_info, machine)
594 disk_point = display_fields.index('DISK_INFO')
595 display_fields = (display_fields[:disk_point] + disk_fields +
596 display_fields[disk_point+1:])
598 main_status['memory'] += ' MiB'
599 for field, disp in display_fields:
600 if field in ('uptime', 'cputime') and locals()[field] is not None:
601 fields.append((disp, locals()[field]))
602 elif field in machine_info:
603 fields.append((disp, machine_info[field]))
604 elif field in main_status:
605 fields.append((disp, main_status[field]))
608 #fields.append((disp, None))
610 checkpoint.checkpoint('Got fields')
613 max_mem = validation.maxMemory(machine.owner, state, machine, False)
614 checkpoint.checkpoint('Got mem')
615 max_disk = validation.maxDisk(machine.owner, machine)
616 defaults = Defaults()
617 for name in 'machine_id name description administrator owner memory contact'.split():
618 setattr(defaults, name, getattr(machine, name))
619 defaults.type = machine.type.type_id
620 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
621 checkpoint.checkpoint('Got defaults')
622 d = dict(user=username,
623 on=status is not None,
634 def unauthFront(_, _2, _3, fields):
635 """Information for unauth'd users."""
636 return templates.unauth(searchList=[{'simple' : True,
637 'hostname' : socket.getfqdn()}])
639 def admin(username, state, path, fields):
641 return ({'Status': '303 See Other',
642 'Location': 'admin/'},
643 "You shouldn't see this message.")
644 if not username in getAfsGroupMembers(config.adminacl, 'athena.mit.edu'):
645 raise InvalidInput('username', username,
646 'Not in admin group %s.' % config.adminacl)
647 newstate = State(username, isadmin=True)
648 newstate.environ = state.environ
649 return handler(username, newstate, path, fields)
651 def throwError(_, __, ___, ____):
652 """Throw an error, to test the error-tracing mechanisms."""
653 raise RuntimeError("test of the emergency broadcast system")
660 errortest=throwError)
662 def printHeaders(headers):
663 """Print a dictionary as HTTP headers."""
664 for key, value in headers.iteritems():
665 print '%s: %s' % (key, value)
668 def send_error_mail(subject, body):
671 to = config.web.errormail
677 """ % (to, config.web.hostname, subject, body)
678 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
679 stdin=subprocess.PIPE)
684 def show_error(op, username, fields, err, emsg, traceback):
685 """Print an error page when an exception occurs"""
686 d = dict(op=op, user=username, fields=fields,
687 errorMessage=str(err), stderr=emsg, traceback=traceback)
688 details = templates.error_raw(searchList=[d])
689 exclude = config.web.errormail_exclude
690 if username not in exclude and '*' not in exclude:
691 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
693 d['details'] = details
694 return templates.error(searchList=[d])
696 def handler(username, state, path, fields):
697 operation, path = pathSplit(path)
700 print 'Starting', operation
701 fun = mapping.get(operation, badOperation)
702 return fun(username, state, path, fields)
705 def __init__(self, environ, start_response):
706 self.environ = environ
707 self.start = start_response
709 self.username = getUser(environ)
710 self.state = State(self.username)
711 self.state.environ = environ
716 start_time = time.time()
717 database.clear_cache()
718 sys.stderr = StringIO()
719 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
720 operation = self.environ.get('PATH_INFO', '')
722 self.start("301 Moved Permanently", [('Location', './')])
724 if self.username is None:
728 checkpoint.checkpoint('Before')
729 output = handler(self.username, self.state, operation, fields)
730 checkpoint.checkpoint('After')
732 headers = dict(DEFAULT_HEADERS)
733 if isinstance(output, tuple):
734 new_headers, output = output
735 headers.update(new_headers)
736 e = revertStandardError()
738 if hasattr(output, 'addError'):
741 # This only happens on redirects, so it'd be a pain to get
742 # the message to the user. Maybe in the response is useful.
743 output = output + '\n\nstderr:\n' + e
744 output_string = str(output)
745 checkpoint.checkpoint('output as a string')
746 except Exception, err:
747 if not fields.has_key('js'):
748 if isinstance(err, InvalidInput):
749 self.start('200 OK', [('Content-Type', 'text/html')])
750 e = revertStandardError()
751 yield str(invalidInput(operation, self.username, fields,
755 self.start('500 Internal Server Error',
756 [('Content-Type', 'text/html')])
757 e = revertStandardError()
758 s = show_error(operation, self.username, fields,
759 err, e, traceback.format_exc())
762 status = headers.setdefault('Status', '200 OK')
763 del headers['Status']
764 self.start(status, headers.items())
766 if fields.has_key('timedebug'):
767 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
774 from flup.server.fcgi_fork import WSGIServer
775 WSGIServer(constructor()).run()
777 if __name__ == '__main__':