2 """Main CGI script for web interface"""
4 from __future__ import with_statement
20 from cherrypy import _cperror
21 from StringIO import StringIO
24 """Revert stderr to stdout, and print the contents of stderr"""
25 if isinstance(sys.stderr, StringIO):
26 print revertStandardError()
28 if __name__ == '__main__':
30 atexit.register(printError)
34 from webcommon import State
36 from getafsgroups import getAfsGroupMembers
37 from invirt import database
38 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
39 from invirt.config import structs as config
40 from invirt.common import InvalidInput, CodeError
42 from view import View, revertStandardError
46 static_dir = os.path.join(os.path.dirname(__file__), 'static')
47 InvirtStatic = cherrypy.tools.staticdir.handler(
52 class InvirtUnauthWeb(View):
56 @cherrypy.tools.mako(filename="/unauth.mako")
58 return {'simple': True}
60 class InvirtWeb(View):
62 super(self.__class__,self).__init__()
64 self._cp_config['tools.require_login.on'] = True
65 self._cp_config['tools.catch_stderr.on'] = True
66 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
67 'from invirt import database']
68 self._cp_config['request.error_response'] = self.handle_error
73 @cherrypy.tools.mako(filename="/invalid.mako")
74 def invalidInput(self):
75 """Print an error page when an InvalidInput exception occurs"""
76 err = cherrypy.request.prev.params["err"]
77 emsg = cherrypy.request.prev.params["emsg"]
78 d = dict(err_field=err.err_field,
79 err_value=str(err.err_value), stderr=emsg,
80 errorMessage=str(err))
84 @cherrypy.tools.mako(filename="/error.mako")
86 """Print an error page when an exception occurs"""
87 op = cherrypy.request.prev.path_info
88 username = cherrypy.request.login
89 err = cherrypy.request.prev.params["err"]
90 emsg = cherrypy.request.prev.params["emsg"]
91 traceback = cherrypy.request.prev.params["traceback"]
92 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
93 errorMessage=str(err), stderr=emsg, traceback=traceback)
94 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
95 details = error_raw.render(**d)
96 exclude = config.web.errormail_exclude
97 if username not in exclude and '*' not in exclude:
98 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
100 d['details'] = details
103 def __getattr__(self, name):
104 if name in ("admin", "overlord"):
105 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
106 raise InvalidInput('username', cherrypy.request.login,
107 'Not in admin group %s.' % config.adminacl)
108 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
111 return super(InvirtWeb, self).__getattr__(name)
113 def handle_error(self):
114 err = sys.exc_info()[1]
115 if isinstance(err, InvalidInput):
116 cherrypy.request.params['err'] = err
117 cherrypy.request.params['emsg'] = revertStandardError()
118 raise cherrypy.InternalRedirect('/invalidInput')
119 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
120 cherrypy.request.params['err'] = err
121 cherrypy.request.params['emsg'] = revertStandardError()
122 cherrypy.request.params['traceback'] = _cperror.format_exc()
123 raise cherrypy.InternalRedirect('/error')
124 # fall back to cherrypy default error page
125 cherrypy.HTTPError(500).set_response()
128 @cherrypy.tools.mako(filename="/list.mako")
129 def list(self, result=None):
130 """Handler for list requests."""
131 d = getListDict(cherrypy.request.login, cherrypy.request.state)
132 if result is not None:
138 @cherrypy.tools.mako(filename="/help.mako")
139 def help(self, subject=None, simple=False):
140 """Handler for help messages."""
144 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
145 ParaVM. You can access the resulting system by logging into the <a
146 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
147 with your Kerberos tickets; there is no root password so sshd will
150 <p>Under the covers, the autoinstaller uses our own patched version of
151 xen-create-image, which is a tool based on debootstrap. If you log
152 into the serial console while the install is running, you can watch
155 'ParaVM Console': """
156 ParaVM machines do not support local console access over VNC. To
157 access the serial console of these machines, you can SSH with Kerberos
158 to %s, using the name of the machine as your
159 username.""" % config.console.hostname,
161 HVM machines use the virtualization features of the processor, while
162 ParaVM machines rely on a modified kernel to communicate directly with
163 the hypervisor. HVMs support boot CDs of any operating system, and
164 the VNC console applet. The three-minute autoinstaller produces
165 ParaVMs. ParaVMs typically are more efficient, and always support the
166 <a href="help?subject=ParaVM+Console">console server</a>.</p>
168 <p>More details are <a
169 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
170 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
171 (which you can skip by using the autoinstaller to begin with.)</p>
173 <p>We recommend using a ParaVM when possible and an HVM when necessary.
176 Don't ask us! We're as mystified as you are.""",
178 The owner field is used to determine <a
179 href="help?subject=Quotas">quotas</a>. It must be the name of a
180 locker that you are an AFS administrator of. In particular, you or an
181 AFS group you are a member of must have AFS rlidwka bits on the
182 locker. You can check who administers the LOCKER locker using the
183 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
184 href="help?subject=Administrator">administrator</a>.""",
186 The administrator field determines who can access the console and
187 power on and off the machine. This can be either a user or a moira
190 Quotas are determined on a per-locker basis. Each locker may have a
191 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
194 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
195 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
196 your machine will run just fine, but the applet's display of the
197 console will suffer artifacts.
200 <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>
201 <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.
206 subject = sorted(help_mapping.keys())
207 if not isinstance(subject, list):
210 return dict(simple=simple,
212 mapping=help_mapping)
213 help._cp_config['tools.require_login.on'] = False
215 def parseCreate(self, fields):
216 kws = dict([(kw, fields[kw]) for kw in
217 'name description owner memory disksize vmtype cdrom autoinstall'.split()
219 validate = validation.Validate(cherrypy.request.login,
220 cherrypy.request.state,
222 return dict(contact=cherrypy.request.login, name=validate.name,
223 description=validate.description, memory=validate.memory,
224 disksize=validate.disksize, owner=validate.owner,
225 machine_type=getattr(validate, 'vmtype', Defaults.type),
226 cdrom=getattr(validate, 'cdrom', None),
227 autoinstall=getattr(validate, 'autoinstall', None))
230 @cherrypy.tools.mako(filename="/list.mako")
231 @cherrypy.tools.require_POST()
232 def create(self, **fields):
233 """Handler for create requests."""
235 parsed_fields = self.parseCreate(fields)
236 machine = controls.createVm(cherrypy.request.login,
237 cherrypy.request.state, **parsed_fields)
238 except InvalidInput, err:
242 cherrypy.request.state.clear() #Changed global state
243 d = getListDict(cherrypy.request.login, cherrypy.request.state)
246 for field, value in fields.items():
247 setattr(d['defaults'], field, value)
249 d['new_machine'] = parsed_fields['name']
253 @cherrypy.tools.mako(filename="/helloworld.mako")
254 def helloworld(self, **kwargs):
255 return {'request': cherrypy.request, 'kwargs': kwargs}
256 helloworld._cp_config['tools.require_login.on'] = False
260 """Throw an error, to test the error-tracing mechanisms."""
261 print >>sys.stderr, "look ma, it's a stderr"
262 raise RuntimeError("test of the emergency broadcast system")
264 class MachineView(View):
265 def __getattr__(self, name):
266 """Synthesize attributes to allow RESTful URLs like
267 /machine/13/info. This is hairy. CherryPy 3.2 adds a
268 method called _cp_dispatch that allows you to explicitly
269 handle URLs that can't be mapped, and it allows you to
270 rewrite the path components and continue processing.
272 This function gets the next path component being resolved
273 as a string. _cp_dispatch will get an array of strings
274 representing any subsequent path components as well."""
277 cherrypy.request.params['machine_id'] = int(name)
283 @cherrypy.tools.mako(filename="/info.mako")
284 def info(self, machine_id):
285 """Handler for info on a single VM."""
286 machine = validation.Validate(cherrypy.request.login,
287 cherrypy.request.state,
288 machine_id=machine_id).machine
289 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):
325 Note that due to same-domain restrictions, the applet connects to
326 the webserver, which needs to forward those requests to the xen
327 server. The Xen server runs another proxy that (1) authenticates
328 and (2) finds the correct port for the VM.
330 You might want iptables like:
332 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
333 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
334 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
335 --dport 10003 -j SNAT --to-source 18.187.7.142
336 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
337 --dport 10003 -j ACCEPT
339 Remember to enable iptables!
340 echo 1 > /proc/sys/net/ipv4/ip_forward
342 machine = validation.Validate(cherrypy.request.login,
343 cherrypy.request.state,
344 machine_id=machine_id).machine
345 token = controls.vnctoken(machine)
346 host = controls.listHost(machine)
348 port = 10003 + [h.hostname for h in config.hosts].index(host)
352 status = controls.statusInfo(machine)
353 has_vnc = hasVnc(status)
358 hostname=cherrypy.request.local.name,
364 @cherrypy.tools.mako(filename="/command.mako")
365 @cherrypy.tools.require_POST()
366 def command(self, command_name, machine_id, **kwargs):
367 """Handler for running commands like boot and delete on a VM."""
368 back = kwargs.get('back')
369 if command_name == 'delete':
372 d = controls.commandResult(cherrypy.request.login,
373 cherrypy.request.state,
374 command_name, machine_id, kwargs)
375 except InvalidInput, err:
378 print >> sys.stderr, err
385 cherrypy.request.state.clear() #Changed global state
386 raise cherrypy.InternalRedirect('/list?result=%s'
387 % urllib.quote(result))
389 raise cherrypy.HTTPRedirect(cherrypy.request.base
390 + '/machine/%d/' % machine_id,
393 raise InvalidInput('back', back, 'Not a known back page.')
395 atmulti = ajaxterm.Multiplex()
397 atsessions_lock = threading.Lock()
400 @cherrypy.tools.mako(filename="/terminal.mako")
401 def terminal(self, machine_id):
402 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
404 status = controls.statusInfo(machine)
405 has_vnc = hasVnc(status)
410 hostname=cherrypy.request.local.name)
414 @cherrypy.tools.require_POST()
415 @cherrypy.tools.gzip()
416 def at(self, machine_id, k=None, c=0, force=0):
417 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
418 with self.atsessions_lock:
419 if machine_id in self.atsessions:
420 term = self.atsessions[machine_id]
422 print >>sys.stderr, "spawning new session for terminal to ",machine_id
423 term = self.atmulti.create(
424 ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
426 # Clear out old sessions when fd is reused
427 for key in self.atsessions:
428 if self.atsessions[key] == term:
429 del self.atsessions[key]
430 self.atsessions[machine_id] = term
432 self.atmulti.proc_write(term,k)
434 dump=self.atmulti.dump(term,c,int(force))
435 cherrypy.response.headers['Content-Type']='text/xml'
436 if isinstance(dump,str):
439 print "Removing session for", machine_id
440 del self.atsessions[machine_id]
441 return '<?xml version="1.0"?><idem></idem>'
443 machine = MachineView()
447 """Class to store default values for fields."""
457 def __init__(self, max_memory=None, max_disk=None, **kws):
458 if max_memory is not None:
459 self.memory = min(self.memory, max_memory)
460 if max_disk is not None:
461 self.disk = min(self.disk, max_disk)
463 setattr(self, key, kws[key])
466 """Does the machine with a given status list support VNC?"""
470 if l[0] == 'device' and l[1][0] == 'vfb':
472 return 'location' in d
476 def getListDict(username, state):
477 """Gets the list of local variables used by list.tmpl."""
478 machines = state.machines
482 xmlist = state.xmlist
488 m.uptime = xmlist[m]['uptime']
489 installing[m] = bool(xmlist[m].get('autoinstall'))
490 if xmlist[m]['console']:
495 has_vnc[m] = "ParaVM"
496 max_memory = validation.maxMemory(username, state)
497 max_disk = validation.maxDisk(username)
498 defaults = Defaults(max_memory=max_memory,
501 def sortkey(machine):
502 return (machine.owner != username, machine.owner, machine.name)
503 machines = sorted(machines, key=sortkey)
504 d = dict(user=username,
505 cant_add_vm=validation.cantAddVm(username, state),
506 max_memory=max_memory,
511 installing=installing)
514 def getHostname(nic):
515 """Find the hostname associated with a NIC.
517 XXX this should be merged with the similar logic in DNS and DHCP.
520 hostname = nic.hostname
522 hostname = nic.machine.name
528 return hostname + '.' + config.dns.domains[0]
530 def getNicInfo(data_dict, machine):
531 """Helper function for info, get data on nics for a machine.
533 Modifies data_dict to include the relevant data, and returns a list
534 of (key, name) pairs to display "name: data_dict[key]" to the user.
536 data_dict['num_nics'] = len(machine.nics)
537 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
538 ('nic%s_mac', 'NIC %s MAC Addr'),
539 ('nic%s_ip', 'NIC %s IP'),
542 for i in range(len(machine.nics)):
543 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
544 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
545 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
546 data_dict['nic%s_ip' % i] = machine.nics[i].ip
547 if len(machine.nics) == 1:
548 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
551 def getDiskInfo(data_dict, machine):
552 """Helper function for info, get data on disks for a machine.
554 Modifies data_dict to include the relevant data, and returns a list
555 of (key, name) pairs to display "name: data_dict[key]" to the user.
557 data_dict['num_disks'] = len(machine.disks)
558 disk_fields_template = [('%s_size', '%s size')]
560 for disk in machine.disks:
561 name = disk.guest_device_name
562 disk_fields.extend([(x % name, y % name) for x, y in
563 disk_fields_template])
564 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
567 def modifyDict(username, state, machine_id, fields):
568 """Modify a machine as specified by CGI arguments.
570 Return a dict containing the machine that was modified.
575 kws = dict([(kw, fields[kw]) for kw in
576 'owner admin contact name description memory vmtype disksize'.split()
578 kws['machine_id'] = machine_id
579 validate = validation.Validate(username, state, **kws)
580 machine = validate.machine
581 oldname = machine.name
583 if hasattr(validate, 'memory'):
584 machine.memory = validate.memory
586 if hasattr(validate, 'vmtype'):
587 machine.type = validate.vmtype
589 if hasattr(validate, 'disksize'):
590 disksize = validate.disksize
591 disk = machine.disks[0]
592 if disk.size != disksize:
593 olddisk[disk.guest_device_name] = disksize
595 session.save_or_update(disk)
598 if hasattr(validate, 'owner') and validate.owner != machine.owner:
599 machine.owner = validate.owner
601 if hasattr(validate, 'name'):
602 machine.name = validate.name
603 for n in machine.nics:
604 if n.hostname == oldname:
605 n.hostname = validate.name
606 if hasattr(validate, 'description'):
607 machine.description = validate.description
608 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
609 machine.administrator = validate.admin
611 if hasattr(validate, 'contact'):
612 machine.contact = validate.contact
614 session.save_or_update(machine)
616 cache_acls.refreshMachine(machine)
621 for diskname in olddisk:
622 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
623 if hasattr(validate, 'name'):
624 controls.renameMachine(machine, oldname, validate.name)
625 return dict(machine=machine)
627 def infoDict(username, state, machine):
628 """Get the variables used by info.tmpl."""
629 status = controls.statusInfo(machine)
630 has_vnc = hasVnc(status)
632 main_status = dict(name=machine.name,
633 memory=str(machine.memory))
637 main_status = dict(status[1:])
638 main_status['host'] = controls.listHost(machine)
639 start_time = float(main_status.get('start_time', 0))
640 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
641 cpu_time_float = float(main_status.get('cpu_time', 0))
642 cputime = datetime.timedelta(seconds=int(cpu_time_float))
643 display_fields = [('name', 'Name'),
644 ('description', 'Description'),
646 ('administrator', 'Administrator'),
647 ('contact', 'Contact'),
650 ('uptime', 'uptime'),
651 ('cputime', 'CPU usage'),
652 ('host', 'Hosted on'),
655 ('state', 'state (xen format)'),
659 machine_info['name'] = machine.name
660 machine_info['description'] = machine.description
661 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
662 machine_info['owner'] = machine.owner
663 machine_info['administrator'] = machine.administrator
664 machine_info['contact'] = machine.contact
666 nic_fields = getNicInfo(machine_info, machine)
667 nic_point = display_fields.index('NIC_INFO')
668 display_fields = (display_fields[:nic_point] + nic_fields +
669 display_fields[nic_point+1:])
671 disk_fields = getDiskInfo(machine_info, machine)
672 disk_point = display_fields.index('DISK_INFO')
673 display_fields = (display_fields[:disk_point] + disk_fields +
674 display_fields[disk_point+1:])
676 main_status['memory'] += ' MiB'
677 for field, disp in display_fields:
678 if field in ('uptime', 'cputime') and locals()[field] is not None:
679 fields.append((disp, locals()[field]))
680 elif field in machine_info:
681 fields.append((disp, machine_info[field]))
682 elif field in main_status:
683 fields.append((disp, main_status[field]))
686 #fields.append((disp, None))
688 max_mem = validation.maxMemory(machine.owner, state, machine, False)
689 max_disk = validation.maxDisk(machine.owner, machine)
690 defaults = Defaults()
691 for name in 'machine_id name description administrator owner memory contact'.split():
692 if getattr(machine, name):
693 setattr(defaults, name, getattr(machine, name))
694 defaults.type = machine.type.type_id
695 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
696 d = dict(user=username,
697 on=status is not None,
708 def send_error_mail(subject, body):
711 to = config.web.errormail
717 """ % (to, config.web.hostname, subject, body)
718 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
719 stdin=subprocess.PIPE)