2 """Main CGI script for web interface"""
16 from cherrypy import _cperror
17 from StringIO import StringIO
20 """Revert stderr to stdout, and print the contents of stderr"""
21 if isinstance(sys.stderr, StringIO):
22 print revertStandardError()
24 if __name__ == '__main__':
26 atexit.register(printError)
30 from webcommon import State
32 from getafsgroups import getAfsGroupMembers
33 from invirt import database
34 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
35 from invirt.config import structs as config
36 from invirt.common import InvalidInput, CodeError
38 from view import View, revertStandardError
40 class InvirtUnauthWeb(View):
42 @cherrypy.tools.mako(filename="/unauth.mako")
44 return {'simple': True}
46 class InvirtWeb(View):
48 super(self.__class__,self).__init__()
50 self._cp_config['tools.require_login.on'] = True
51 self._cp_config['tools.catch_stderr.on'] = True
52 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
53 'from invirt import database']
54 self._cp_config['request.error_response'] = self.handle_error
57 @cherrypy.tools.mako(filename="/invalid.mako")
58 def invalidInput(self):
59 """Print an error page when an InvalidInput exception occurs"""
60 err = cherrypy.request.prev.params["err"]
61 emsg = cherrypy.request.prev.params["emsg"]
62 d = dict(err_field=err.err_field,
63 err_value=str(err.err_value), stderr=emsg,
64 errorMessage=str(err))
68 @cherrypy.tools.mako(filename="/error.mako")
70 #op, username, fields, err, emsg, traceback):
71 """Print an error page when an exception occurs"""
72 op = cherrypy.request.prev.path_info
73 username = cherrypy.request.login
74 err = cherrypy.request.prev.params["err"]
75 emsg = cherrypy.request.prev.params["emsg"]
76 traceback = cherrypy.request.prev.params["traceback"]
77 d = dict(op = op, user=username, fields=cherrypy.request.prev.params,
78 errorMessage=str(err), stderr=emsg, traceback=traceback)
79 error_raw = cherrypy.tools.mako.callable.get_lookup(**cherrypy.tools.mako._merged_args()).get_template("/error_raw.mako")
80 details = error_raw.render(**d)
81 exclude = config.web.errormail_exclude
82 if username not in exclude and '*' not in exclude:
83 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
85 d['details'] = details
88 def __getattr__(self, name):
89 if name in ("admin", "overlord"):
90 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz[0].cell):
91 raise InvalidInput('username', cherrypy.request.login,
92 'Not in admin group %s.' % config.adminacl)
93 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
96 return super(InvirtWeb, self).__getattr__(name)
98 def handle_error(self):
99 err = sys.exc_info()[1]
100 if isinstance(err, InvalidInput):
101 e = revertStandardError()
102 cherrypy.request.params['err'] = err
103 cherrypy.request.params['emsg'] = e
104 raise cherrypy.InternalRedirect('/invalidInput')
105 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
106 e = revertStandardError()
107 cherrypy.request.params['err'] = err
108 cherrypy.request.params['emsg'] = e
109 cherrypy.request.params['traceback'] = _cperror.format_exc()
110 raise cherrypy.InternalRedirect('/error')
111 # fall back to cherrypy default error page
112 cherrypy.HTTPError(500).set_response()
115 @cherrypy.tools.mako(filename="/list.mako")
116 def list(self, result=None):
117 """Handler for list requests."""
118 checkpoint.checkpoint('Getting list dict')
119 d = getListDict(cherrypy.request.login, cherrypy.request.state)
120 if result is not None:
122 checkpoint.checkpoint('Got list dict')
127 @cherrypy.tools.mako(filename="/help.mako")
128 def help(self, subject=None, simple=False):
129 """Handler for help messages."""
133 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
134 ParaVM. You can access the resulting system by logging into the <a
135 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
136 with your Kerberos tickets; there is no root password so sshd will
139 <p>Under the covers, the autoinstaller uses our own patched version of
140 xen-create-image, which is a tool based on debootstrap. If you log
141 into the serial console while the install is running, you can watch
144 'ParaVM Console': """
145 ParaVM machines do not support local console access over VNC. To
146 access the serial console of these machines, you can SSH with Kerberos
147 to %s, using the name of the machine as your
148 username.""" % config.console.hostname,
150 HVM machines use the virtualization features of the processor, while
151 ParaVM machines rely on a modified kernel to communicate directly with
152 the hypervisor. HVMs support boot CDs of any operating system, and
153 the VNC console applet. The three-minute autoinstaller produces
154 ParaVMs. ParaVMs typically are more efficient, and always support the
155 <a href="help?subject=ParaVM+Console">console server</a>.</p>
157 <p>More details are <a
158 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
159 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
160 (which you can skip by using the autoinstaller to begin with.)</p>
162 <p>We recommend using a ParaVM when possible and an HVM when necessary.
165 Don't ask us! We're as mystified as you are.""",
167 The owner field is used to determine <a
168 href="help?subject=Quotas">quotas</a>. It must be the name of a
169 locker that you are an AFS administrator of. In particular, you or an
170 AFS group you are a member of must have AFS rlidwka bits on the
171 locker. You can check who administers the LOCKER locker using the
172 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
173 href="help?subject=Administrator">administrator</a>.""",
175 The administrator field determines who can access the console and
176 power on and off the machine. This can be either a user or a moira
179 Quotas are determined on a per-locker basis. Each locker may have a
180 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
183 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
184 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
185 your machine will run just fine, but the applet's display of the
186 console will suffer artifacts.
189 <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>
190 <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.
195 subject = sorted(help_mapping.keys())
196 if not isinstance(subject, list):
199 return dict(simple=simple,
201 mapping=help_mapping)
202 help._cp_config['tools.require_login.on'] = False
204 def parseCreate(self, fields):
205 kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
206 validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
207 return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
208 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
209 cdrom=getattr(validate, 'cdrom', None),
210 autoinstall=getattr(validate, 'autoinstall', None))
213 @cherrypy.tools.mako(filename="/list.mako")
214 @cherrypy.tools.require_POST()
215 def create(self, **fields):
216 """Handler for create requests."""
218 parsed_fields = self.parseCreate(fields)
219 machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
220 except InvalidInput, err:
224 cherrypy.request.state.clear() #Changed global state
225 d = getListDict(cherrypy.request.login, cherrypy.request.state)
228 for field in fields.keys():
229 setattr(d['defaults'], field, fields.get(field))
231 d['new_machine'] = parsed_fields['name']
235 @cherrypy.tools.mako(filename="/helloworld.mako")
236 def helloworld(self, **kwargs):
237 return {'request': cherrypy.request, 'kwargs': kwargs}
238 helloworld._cp_config['tools.require_login.on'] = False
242 """Throw an error, to test the error-tracing mechanisms."""
243 print >>sys.stderr, "look ma, it's a stderr"
244 raise RuntimeError("test of the emergency broadcast system")
246 class MachineView(View):
247 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
248 # _cp_dispatch, and parse the argument as a list instead of
251 def __getattr__(self, name):
253 machine_id = int(name)
254 cherrypy.request.params['machine_id'] = machine_id
260 @cherrypy.tools.mako(filename="/info.mako")
261 def info(self, machine_id):
262 """Handler for info on a single VM."""
263 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
264 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
265 checkpoint.checkpoint('Got infodict')
270 @cherrypy.tools.mako(filename="/info.mako")
271 @cherrypy.tools.require_POST()
272 def modify(self, machine_id, **fields):
273 """Handler for modifying attributes of a machine."""
275 modify_dict = modifyDict(cherrypy.request.login, cherrypy.request.state, machine_id, fields)
276 except InvalidInput, err:
278 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
280 machine = modify_dict['machine']
283 info_dict = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
284 info_dict['err'] = err
286 for field in fields.keys():
287 setattr(info_dict['defaults'], field, fields.get(field))
288 info_dict['result'] = result
292 @cherrypy.tools.mako(filename="/vnc.mako")
293 def vnc(self, machine_id):
296 Note that due to same-domain restrictions, the applet connects to
297 the webserver, which needs to forward those requests to the xen
298 server. The Xen server runs another proxy that (1) authenticates
299 and (2) finds the correct port for the VM.
301 You might want iptables like:
303 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
304 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
305 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
306 --dport 10003 -j SNAT --to-source 18.187.7.142
307 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
308 --dport 10003 -j ACCEPT
310 Remember to enable iptables!
311 echo 1 > /proc/sys/net/ipv4/ip_forward
313 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
315 token = controls.vnctoken(machine)
316 host = controls.listHost(machine)
318 port = 10003 + [h.hostname for h in config.hosts].index(host)
322 status = controls.statusInfo(machine)
323 has_vnc = hasVnc(status)
328 hostname=cherrypy.request.local.name,
333 @cherrypy.tools.mako(filename="/command.mako")
334 @cherrypy.tools.require_POST()
335 def command(self, command_name, machine_id, **kwargs):
336 """Handler for running commands like boot and delete on a VM."""
337 back = kwargs.get('back', None)
339 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
340 if d['command'] == 'Delete VM':
342 except InvalidInput, err:
345 print >> sys.stderr, err
352 cherrypy.request.state.clear() #Changed global state
353 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
355 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
357 raise InvalidInput('back', back, 'Not a known back page.')
359 machine = MachineView()
363 self.start_time = time.time()
364 self.checkpoints = []
366 def checkpoint(self, s):
367 self.checkpoints.append((s, time.time()))
370 return ('Timing info:\n%s\n' %
371 '\n'.join(['%s: %s' % (d, t - self.start_time) for
372 (d, t) in self.checkpoints]))
374 checkpoint = Checkpoint()
377 """Class to store default values for fields."""
387 def __init__(self, max_memory=None, max_disk=None, **kws):
388 if max_memory is not None:
389 self.memory = min(self.memory, max_memory)
390 if max_disk is not None:
391 self.disk = min(self.disk, max_disk)
393 setattr(self, key, kws[key])
396 """Does the machine with a given status list support VNC?"""
400 if l[0] == 'device' and l[1][0] == 'vfb':
402 return 'location' in d
406 def getListDict(username, state):
407 """Gets the list of local variables used by list.tmpl."""
408 checkpoint.checkpoint('Starting')
409 machines = state.machines
410 checkpoint.checkpoint('Got my machines')
414 xmlist = state.xmlist
415 checkpoint.checkpoint('Got uptimes')
421 m.uptime = xmlist[m]['uptime']
422 if xmlist[m]['console']:
427 has_vnc[m] = "ParaVM"
428 if xmlist[m].get('autoinstall'):
431 installing[m] = False
432 max_memory = validation.maxMemory(username, state)
433 max_disk = validation.maxDisk(username)
434 checkpoint.checkpoint('Got max mem/disk')
435 defaults = Defaults(max_memory=max_memory,
438 checkpoint.checkpoint('Got defaults')
439 def sortkey(machine):
440 return (machine.owner != username, machine.owner, machine.name)
441 machines = sorted(machines, key=sortkey)
442 d = dict(user=username,
443 cant_add_vm=validation.cantAddVm(username, state),
444 max_memory=max_memory,
449 installing=installing)
452 def getHostname(nic):
453 """Find the hostname associated with a NIC.
455 XXX this should be merged with the similar logic in DNS and DHCP.
458 hostname = nic.hostname
460 hostname = nic.machine.name
466 return hostname + '.' + config.dns.domains[0]
468 def getNicInfo(data_dict, machine):
469 """Helper function for info, get data on nics for a machine.
471 Modifies data_dict to include the relevant data, and returns a list
472 of (key, name) pairs to display "name: data_dict[key]" to the user.
474 data_dict['num_nics'] = len(machine.nics)
475 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
476 ('nic%s_mac', 'NIC %s MAC Addr'),
477 ('nic%s_ip', 'NIC %s IP'),
480 for i in range(len(machine.nics)):
481 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
482 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
483 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
484 data_dict['nic%s_ip' % i] = machine.nics[i].ip
485 if len(machine.nics) == 1:
486 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
489 def getDiskInfo(data_dict, machine):
490 """Helper function for info, get data on disks for a machine.
492 Modifies data_dict to include the relevant data, and returns a list
493 of (key, name) pairs to display "name: data_dict[key]" to the user.
495 data_dict['num_disks'] = len(machine.disks)
496 disk_fields_template = [('%s_size', '%s size')]
498 for disk in machine.disks:
499 name = disk.guest_device_name
500 disk_fields.extend([(x % name, y % name) for x, y in
501 disk_fields_template])
502 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
505 def modifyDict(username, state, machine_id, fields):
506 """Modify a machine as specified by CGI arguments.
508 Return a dict containing the machine that was modified.
513 kws = dict([(kw, fields.get(kw)) for kw in 'owner admin contact name description memory vmtype disksize'.split() if fields.get(kw)])
514 kws['machine_id'] = machine_id
515 validate = validation.Validate(username, state, **kws)
516 machine = validate.machine
517 oldname = machine.name
519 if hasattr(validate, 'memory'):
520 machine.memory = validate.memory
522 if hasattr(validate, 'vmtype'):
523 machine.type = validate.vmtype
525 if hasattr(validate, 'disksize'):
526 disksize = validate.disksize
527 disk = machine.disks[0]
528 if disk.size != disksize:
529 olddisk[disk.guest_device_name] = disksize
531 session.save_or_update(disk)
534 if hasattr(validate, 'owner') and validate.owner != machine.owner:
535 machine.owner = validate.owner
537 if hasattr(validate, 'name'):
538 machine.name = validate.name
539 for n in machine.nics:
540 if n.hostname == oldname:
541 n.hostname = validate.name
542 if hasattr(validate, 'description'):
543 machine.description = validate.description
544 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
545 machine.administrator = validate.admin
547 if hasattr(validate, 'contact'):
548 machine.contact = validate.contact
550 session.save_or_update(machine)
552 cache_acls.refreshMachine(machine)
557 for diskname in olddisk:
558 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
559 if hasattr(validate, 'name'):
560 controls.renameMachine(machine, oldname, validate.name)
561 return dict(machine=machine)
563 def infoDict(username, state, machine):
564 """Get the variables used by info.tmpl."""
565 status = controls.statusInfo(machine)
566 checkpoint.checkpoint('Getting status info')
567 has_vnc = hasVnc(status)
569 main_status = dict(name=machine.name,
570 memory=str(machine.memory))
574 main_status = dict(status[1:])
575 main_status['host'] = controls.listHost(machine)
576 start_time = float(main_status.get('start_time', 0))
577 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
578 cpu_time_float = float(main_status.get('cpu_time', 0))
579 cputime = datetime.timedelta(seconds=int(cpu_time_float))
580 checkpoint.checkpoint('Status')
581 display_fields = [('name', 'Name'),
582 ('description', 'Description'),
584 ('administrator', 'Administrator'),
585 ('contact', 'Contact'),
588 ('uptime', 'uptime'),
589 ('cputime', 'CPU usage'),
590 ('host', 'Hosted on'),
593 ('state', 'state (xen format)'),
597 machine_info['name'] = machine.name
598 machine_info['description'] = machine.description
599 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
600 machine_info['owner'] = machine.owner
601 machine_info['administrator'] = machine.administrator
602 machine_info['contact'] = machine.contact
604 nic_fields = getNicInfo(machine_info, machine)
605 nic_point = display_fields.index('NIC_INFO')
606 display_fields = (display_fields[:nic_point] + nic_fields +
607 display_fields[nic_point+1:])
609 disk_fields = getDiskInfo(machine_info, machine)
610 disk_point = display_fields.index('DISK_INFO')
611 display_fields = (display_fields[:disk_point] + disk_fields +
612 display_fields[disk_point+1:])
614 main_status['memory'] += ' MiB'
615 for field, disp in display_fields:
616 if field in ('uptime', 'cputime') and locals()[field] is not None:
617 fields.append((disp, locals()[field]))
618 elif field in machine_info:
619 fields.append((disp, machine_info[field]))
620 elif field in main_status:
621 fields.append((disp, main_status[field]))
624 #fields.append((disp, None))
626 checkpoint.checkpoint('Got fields')
629 max_mem = validation.maxMemory(machine.owner, state, machine, False)
630 checkpoint.checkpoint('Got mem')
631 max_disk = validation.maxDisk(machine.owner, machine)
632 defaults = Defaults()
633 for name in 'machine_id name description administrator owner memory contact'.split():
634 if getattr(machine, name):
635 setattr(defaults, name, getattr(machine, name))
636 defaults.type = machine.type.type_id
637 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
638 checkpoint.checkpoint('Got defaults')
639 d = dict(user=username,
640 on=status is not None,
651 def send_error_mail(subject, body):
654 to = config.web.errormail
660 """ % (to, config.web.hostname, subject, body)
661 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
662 stdin=subprocess.PIPE)