2 """Main CGI script for web interface"""
17 from cherrypy import _cperror
18 from StringIO import StringIO
21 """Revert stderr to stdout, and print the contents of stderr"""
22 if isinstance(sys.stderr, StringIO):
23 print revertStandardError()
25 if __name__ == '__main__':
27 atexit.register(printError)
31 from webcommon import State
33 from getafsgroups import getAfsGroupMembers
34 from invirt import database
35 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
36 from invirt.config import structs as config
37 from invirt.common import InvalidInput, CodeError
39 from view import View, revertStandardError
42 static_dir = os.path.join(os.path.dirname(__file__), 'static')
43 InvirtStatic = cherrypy.tools.staticdir.handler(
48 class InvirtUnauthWeb(View):
52 @cherrypy.tools.mako(filename="/unauth.mako")
54 return dict(simple=True)
56 class InvirtWeb(View):
58 super(self.__class__,self).__init__()
60 self._cp_config['tools.require_login.on'] = True
61 self._cp_config['tools.catch_stderr.on'] = True
62 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
63 'from invirt import database']
64 self._cp_config['request.error_response'] = self.handle_error
69 @cherrypy.tools.mako(filename="/invalid.mako")
70 def invalidInput(self):
71 """Print an error page when an InvalidInput exception occurs"""
72 err = cherrypy.request.prev.params["err"]
73 emsg = cherrypy.request.prev.params["emsg"]
74 d = dict(err_field=err.err_field,
75 err_value=str(err.err_value), stderr=emsg,
76 errorMessage=str(err))
80 @cherrypy.tools.mako(filename="/error.mako")
82 """Print an error page when an exception occurs"""
83 op = cherrypy.request.prev.path_info
84 username = cherrypy.request.login
85 err = cherrypy.request.prev.params["err"]
86 emsg = cherrypy.request.prev.params["emsg"]
87 traceback = cherrypy.request.prev.params["traceback"]
88 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
89 errorMessage=str(err), stderr=emsg, traceback=traceback)
90 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
91 details = error_raw.render(**d)
92 exclude = config.web.errormail_exclude
93 if username not in exclude and '*' not in exclude:
94 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
96 d['details'] = details
99 def __getattr__(self, name):
100 # At the point __getattr__ is called, tools haven't been run. Make sure the user is logged in.
101 cherrypy.tools.remote_user_login.callable()
103 if name in ("admin", "overlord"):
104 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
105 raise InvalidInput('username', cherrypy.request.login,
106 'Not in admin group %s.' % config.adminacl)
107 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
110 return super(InvirtWeb, self).__getattr__(name)
112 def handle_error(self):
113 err = sys.exc_info()[1]
114 if isinstance(err, InvalidInput):
115 cherrypy.request.params['err'] = err
116 cherrypy.request.params['emsg'] = revertStandardError()
117 raise cherrypy.InternalRedirect('/invalidInput')
118 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
119 cherrypy.request.params['err'] = err
120 cherrypy.request.params['emsg'] = revertStandardError()
121 cherrypy.request.params['traceback'] = _cperror.format_exc()
122 raise cherrypy.InternalRedirect('/error')
123 # fall back to cherrypy default error page
124 cherrypy.HTTPError(500).set_response()
127 @cherrypy.tools.mako(filename="/list.mako")
128 def list(self, result=None):
129 """Handler for list requests."""
130 d = getListDict(cherrypy.request.login, cherrypy.request.state)
131 if result is not None:
137 @cherrypy.tools.mako(filename="/help.mako")
138 def help(self, subject=None, simple=False):
139 """Handler for help messages."""
143 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
144 ParaVM. You can access the resulting system by logging into the <a
145 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
146 with your Kerberos tickets; there is no root password so sshd will
149 <p>Under the covers, the autoinstaller uses our own patched version of
150 xen-create-image, which is a tool based on debootstrap. If you log
151 into the serial console while the install is running, you can watch
154 'ParaVM Console': """
155 ParaVM machines do not support local console access over VNC. To
156 access the serial console of these machines, you can SSH with Kerberos
157 to %s, using the name of the machine as your
158 username.""" % config.console.hostname,
160 HVM machines use the virtualization features of the processor, while
161 ParaVM machines rely on a modified kernel to communicate directly with
162 the hypervisor. HVMs support boot CDs of any operating system, and
163 the VNC console applet. The three-minute autoinstaller produces
164 ParaVMs. ParaVMs typically are more efficient, and always support the
165 <a href="help?subject=ParaVM+Console">console server</a>.</p>
167 <p>More details are <a
168 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
169 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
170 (which you can skip by using the autoinstaller to begin with.)</p>
172 <p>We recommend using a ParaVM when possible and an HVM when necessary.
175 The owner field is used to determine <a
176 href="help?subject=Quotas">quotas</a>. It must be the name of a
177 locker that you are an AFS administrator of. In particular, you or an
178 AFS group you are a member of must have AFS rlidwka bits on the
179 locker. You can check who administers the LOCKER locker using the
180 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
181 href="help?subject=Administrator">administrator</a>.""",
183 The administrator field determines who can access the console and
184 power on and off the machine. This can be either a user or a moira
187 Quotas are determined on a per-locker basis. Each locker may have a
188 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
191 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
192 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
193 your machine will run just fine, but the applet's display of the
194 console will suffer artifacts.
197 <strong>Windows 7:</strong> The Windows 7 image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-7.txt">the licensing agreement</a> for details. The installer requires 512 MiB RAM and at least 15 GiB disk space (20 GiB or more recommended).<br>
198 <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>
199 <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.
204 subject = sorted(help_mapping.keys())
205 if not isinstance(subject, list):
208 return dict(simple=simple,
210 mapping=help_mapping)
211 help._cp_config['tools.require_login.on'] = False
213 def parseCreate(self, fields):
214 kws = dict([(kw, fields[kw]) for kw in
215 'name description owner memory disksize vmtype cdrom autoinstall'.split()
217 validate = validation.Validate(cherrypy.request.login,
218 cherrypy.request.state,
220 return dict(contact=cherrypy.request.login, name=validate.name,
221 description=validate.description, memory=validate.memory,
222 disksize=validate.disksize, owner=validate.owner,
223 machine_type=getattr(validate, 'vmtype', Defaults.type),
224 cdrom=getattr(validate, 'cdrom', None),
225 autoinstall=getattr(validate, 'autoinstall', None))
228 @cherrypy.tools.mako(filename="/list.mako")
229 @cherrypy.tools.require_POST()
230 def create(self, **fields):
231 """Handler for create requests."""
233 parsed_fields = self.parseCreate(fields)
234 machine = controls.createVm(cherrypy.request.login,
235 cherrypy.request.state, **parsed_fields)
236 except InvalidInput, err:
240 cherrypy.request.state.clear() #Changed global state
241 d = getListDict(cherrypy.request.login, cherrypy.request.state)
244 for field, value in fields.items():
245 setattr(d['defaults'], field, value)
247 d['new_machine'] = parsed_fields['name']
251 @cherrypy.tools.mako(filename="/helloworld.mako")
252 def helloworld(self, **kwargs):
253 return {'request': cherrypy.request, 'kwargs': kwargs}
254 helloworld._cp_config['tools.require_login.on'] = False
258 """Throw an error, to test the error-tracing mechanisms."""
259 print >>sys.stderr, "look ma, it's a stderr"
260 raise RuntimeError("test of the emergency broadcast system")
262 class MachineView(View):
263 def __getattr__(self, name):
264 """Synthesize attributes to allow RESTful URLs like
265 /machine/13/info. This is hairy. CherryPy 3.2 adds a
266 method called _cp_dispatch that allows you to explicitly
267 handle URLs that can't be mapped, and it allows you to
268 rewrite the path components and continue processing.
270 This function gets the next path component being resolved
271 as a string. _cp_dispatch will get an array of strings
272 representing any subsequent path components as well."""
275 cherrypy.request.params['machine_id'] = int(name)
281 @cherrypy.tools.mako(filename="/info.mako")
282 def info(self, machine_id, result=None):
283 """Handler for info on a single VM."""
284 machine = validation.Validate(cherrypy.request.login,
285 cherrypy.request.state,
286 machine_id=machine_id).machine
287 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
294 @cherrypy.tools.mako(filename="/info.mako")
295 @cherrypy.tools.require_POST()
296 def modify(self, machine_id, **fields):
297 """Handler for modifying attributes of a machine."""
299 modify_dict = modifyDict(cherrypy.request.login,
300 cherrypy.request.state,
302 except InvalidInput, err:
304 machine = validation.Validate(cherrypy.request.login,
305 cherrypy.request.state,
306 machine_id=machine_id).machine
308 machine = modify_dict['machine']
311 info_dict = infoDict(cherrypy.request.login,
312 cherrypy.request.state, machine)
313 info_dict['err'] = err
315 for field, value in fields.items():
316 setattr(info_dict['defaults'], field, value)
317 info_dict['result'] = result
321 @cherrypy.tools.mako(filename="/vnc.mako")
322 def vnc(self, machine_id):
323 """VNC applet page"""
324 return self._vnc(machine_id)
327 @cherrypy.tools.response_headers(headers=[('Content-Disposition', 'attachment; filename=vnc.jnlp')])
328 @cherrypy.tools.mako(filename="/vnc_jnlp.mako", content_type="application/x-java-jnlp-file")
329 def vnc_jnlp(self, machine_id):
330 """VNC applet exposed as a Java Web Start app (JNLP file)"""
331 return self._vnc(machine_id)
333 def _vnc(self, machine_id):
334 """VNC applet page functionality.
336 Note that due to same-domain restrictions, the applet connects to
337 the webserver, which needs to forward those requests to the xen
338 server. The Xen server runs another proxy that (1) authenticates
339 and (2) finds the correct port for the VM.
341 You might want iptables like:
343 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
344 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
345 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
346 --dport 10003 -j SNAT --to-source 18.187.7.142
347 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
348 --dport 10003 -j ACCEPT
350 Remember to enable iptables!
351 echo 1 > /proc/sys/net/ipv4/ip_forward
353 machine = validation.Validate(cherrypy.request.login,
354 cherrypy.request.state,
355 machine_id=machine_id).machine
356 token = controls.vnctoken(machine)
357 host = controls.listHost(machine)
359 port = config.vnc.base_port + [h.hostname for h in config.hosts].index(host)
363 status = controls.statusInfo(machine)
364 has_vnc = hasVnc(status)
369 hostname=cherrypy.request.local.name,
375 @cherrypy.tools.mako(filename="/novnc.mako")
376 def novnc(self, machine_id):
377 """NoVNC page functionality.
379 You need to set config.vnc.novnc_port and run the NoVNC proxy on
382 machine = validation.Validate(cherrypy.request.login,
383 cherrypy.request.state,
384 machine_id=machine_id).machine
385 token = controls.vnctoken(machine)
386 host = controls.listHost(machine)
388 status = controls.statusInfo(machine)
389 has_vnc = hasVnc(status)
394 hostname=cherrypy.request.local.name,
400 @cherrypy.tools.mako(filename="/command.mako")
401 @cherrypy.tools.require_POST()
402 def command(self, command_name, machine_id, **kwargs):
403 """Handler for running commands like boot and delete on a VM."""
404 back = kwargs.get('back')
405 if command_name == 'delete':
408 d = controls.commandResult(cherrypy.request.login,
409 cherrypy.request.state,
410 command_name, machine_id, kwargs)
411 except InvalidInput, err:
414 print >> sys.stderr, err
423 cherrypy.request.state.clear() #Changed global state
424 raise cherrypy.InternalRedirect('/list?result=%s'
425 % urllib.quote(result))
427 url = cherrypy.request.base + '/machine/%d/' % machine_id
429 url += '?result='+urllib.quote(result)
430 raise cherrypy.HTTPRedirect(url,
433 raise InvalidInput('back', back, 'Not a known back page.')
435 machine = MachineView()
439 """Class to store default values for fields."""
449 def __init__(self, max_memory=None, max_disk=None, **kws):
450 if max_memory is not None:
451 self.memory = min(self.memory, max_memory)
452 if max_disk is not None:
453 self.disk = min(self.disk, max_disk)
455 setattr(self, key, kws[key])
458 """Does the machine with a given status list support VNC?"""
462 if l[0] == 'device' and l[1][0] == 'vfb':
464 return 'location' in d
468 def getListDict(username, state):
469 """Gets the list of local variables used by list.tmpl."""
470 machines = state.machines
474 xmlist = state.xmlist
480 m.uptime = xmlist[m]['uptime']
481 installing[m] = bool(xmlist[m].get('autoinstall'))
482 if xmlist[m]['console']:
487 has_vnc[m] = "ParaVM"
488 max_memory = validation.maxMemory(username, state)
489 max_disk = validation.maxDisk(username)
490 defaults = Defaults(max_memory=max_memory,
493 def sortkey(machine):
494 return (machine.owner != username, machine.owner, machine.name)
495 machines = sorted(machines, key=sortkey)
496 d = dict(user=username,
497 cant_add_vm=validation.cantAddVm(username, state),
498 max_memory=max_memory,
503 installing=installing,
504 disable_creation=False)
507 def getHostname(nic):
508 """Find the hostname associated with a NIC.
510 XXX this should be merged with the similar logic in DNS and DHCP.
513 hostname = nic.hostname
515 hostname = nic.machine.name
521 return hostname + '.' + config.dns.domains[0]
523 def getNicInfo(data_dict, machine):
524 """Helper function for info, get data on nics for a machine.
526 Modifies data_dict to include the relevant data, and returns a list
527 of (key, name) pairs to display "name: data_dict[key]" to the user.
529 data_dict['num_nics'] = len(machine.nics)
530 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
531 ('nic%s_mac', 'NIC %s MAC Addr'),
532 ('nic%s_ip', 'NIC %s IP'),
533 ('nic%s_netmask', 'NIC %s Netmask'),
534 ('nic%s_gateway', 'NIC %s Gateway'),
537 for i in range(len(machine.nics)):
538 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
539 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
540 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
541 data_dict['nic%s_ip' % i] = machine.nics[i].ip
542 data_dict['nic%s_netmask' % i] = machine.nics[i].netmask
543 data_dict['nic%s_gateway' % i] = machine.nics[i].gateway
544 if machine.nics[i].other_ip:
545 nic_fields.append(('nic%s_other' % i, 'NIC %s Other Address' % i))
546 other = '%s/%s via %s' % (machine.nics[i].other_ip, machine.nics[i].other_netmask, machine.nics[i].other_gateway)
547 other_action = machine.nics[i].other_action
548 if other_action == 'dnat':
549 other += " (NAT to primary IP)"
550 elif other_action == 'renumber':
551 other += " (cold boot or renew DHCP lease to swap)"
552 elif other_action == 'renumber_dhcp':
553 other += " (renew DHCP lease to swap)"
554 elif other_action == 'remove':
555 other += " (will be removed at next cold boot or DHCP lease renewal)"
557 other += " (pending assignment)"
558 data_dict['nic%s_other' % i] = other
559 if len(machine.nics) == 1:
560 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
563 def getDiskInfo(data_dict, machine):
564 """Helper function for info, get data on disks for a machine.
566 Modifies data_dict to include the relevant data, and returns a list
567 of (key, name) pairs to display "name: data_dict[key]" to the user.
569 data_dict['num_disks'] = len(machine.disks)
570 disk_fields_template = [('%s_size', '%s size')]
572 for disk in machine.disks:
573 name = disk.guest_device_name
574 disk_fields.extend([(x % name, y % name) for x, y in
575 disk_fields_template])
576 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
579 def modifyDict(username, state, machine_id, fields):
580 """Modify a machine as specified by CGI arguments.
582 Return a dict containing the machine that was modified.
587 kws = dict((kw, fields[kw]) for kw in
588 'owner admin contact name description memory vmtype disksize'.split()
590 kws['machine_id'] = machine_id
591 validate = validation.Validate(username, state, **kws)
592 machine = validate.machine
593 oldname = machine.name
595 if hasattr(validate, 'memory'):
596 machine.memory = validate.memory
598 if hasattr(validate, 'vmtype'):
599 machine.type = validate.vmtype
602 if hasattr(validate, 'owner') and validate.owner != machine.owner:
603 machine.owner = validate.owner
605 if hasattr(validate, 'description'):
606 machine.description = validate.description
607 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
608 machine.administrator = validate.admin
610 if hasattr(validate, 'contact'):
611 machine.contact = validate.contact
621 if hasattr(validate, 'disksize'):
622 disksize = validate.disksize
623 disk = machine.disks[0]
624 if disk.size != disksize:
625 olddisk[disk.guest_device_name] = disksize
628 for diskname in olddisk:
629 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
638 if hasattr(validate, 'name'):
639 machine.name = validate.name
640 for n in machine.nics:
641 if n.hostname == oldname:
642 n.hostname = validate.name
643 if hasattr(validate, 'name'):
644 controls.renameMachine(machine, oldname, validate.name)
652 cache_acls.refreshMachine(machine)
654 return dict(machine=machine)
656 def infoDict(username, state, machine):
657 """Get the variables used by info.tmpl."""
659 status = controls.statusInfo(machine)
661 # machine was shut down in between the call to listInfoDict and this
663 has_vnc = hasVnc(status)
665 main_status = dict(name=machine.name,
666 memory=str(machine.memory))
670 main_status = dict(status[1:])
671 main_status['host'] = controls.listHost(machine)
672 start_time = main_status.get('start_time')
673 if start_time is None:
674 uptime = "Still booting?"
676 start_time = float(start_time)
677 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
678 cpu_time_float = float(main_status.get('cpu_time', 0))
679 cputime = datetime.timedelta(seconds=int(cpu_time_float))
680 display_fields = [('name', 'Name'),
681 ('description', 'Description'),
683 ('administrator', 'Administrator'),
684 ('contact', 'Contact'),
687 ('uptime', 'uptime'),
688 ('cputime', 'CPU usage'),
689 ('host', 'Hosted on'),
692 ('state', 'state (xen format)'),
696 machine_info['name'] = machine.name
697 machine_info['description'] = machine.description
698 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
699 machine_info['owner'] = machine.owner
700 machine_info['administrator'] = machine.administrator
701 machine_info['contact'] = machine.contact
703 nic_fields = getNicInfo(machine_info, machine)
704 nic_point = display_fields.index('NIC_INFO')
705 display_fields = (display_fields[:nic_point] + nic_fields +
706 display_fields[nic_point+1:])
708 disk_fields = getDiskInfo(machine_info, machine)
709 disk_point = display_fields.index('DISK_INFO')
710 display_fields = (display_fields[:disk_point] + disk_fields +
711 display_fields[disk_point+1:])
714 for n in machine.nics:
715 if n.other_action == 'renumber_dhcp':
718 main_status['memory'] += ' MiB'
719 for field, disp in display_fields:
720 if field in ('uptime', 'cputime') and locals()[field] is not None:
721 fields.append((disp, locals()[field]))
722 elif field in machine_info:
723 fields.append((disp, machine_info[field]))
724 elif field in main_status:
725 fields.append((disp, main_status[field]))
728 #fields.append((disp, None))
730 max_mem = validation.maxMemory(machine.owner, state, machine, False)
731 max_disk = validation.maxDisk(machine.owner, machine)
732 defaults = Defaults()
733 for name in 'machine_id name description administrator owner memory contact'.split():
734 if getattr(machine, name):
735 setattr(defaults, name, getattr(machine, name))
736 defaults.type = machine.type.type_id
737 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
738 d = dict(user=username,
739 on=status is not None,
751 def send_error_mail(subject, body):
754 to = config.web.errormail
760 """ % (to, config.web.hostname, subject, body)
761 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
762 stdin=subprocess.PIPE)