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 """Print an error page when an exception occurs"""
71 op = cherrypy.request.prev.path_info
72 username = cherrypy.request.login
73 err = cherrypy.request.prev.params["err"]
74 emsg = cherrypy.request.prev.params["emsg"]
75 traceback = cherrypy.request.prev.params["traceback"]
76 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
77 errorMessage=str(err), stderr=emsg, traceback=traceback)
78 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
79 details = error_raw.render(**d)
80 exclude = config.web.errormail_exclude
81 if username not in exclude and '*' not in exclude:
82 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
84 d['details'] = details
87 def __getattr__(self, name):
88 if name in ("admin", "overlord"):
89 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz[0].cell):
90 raise InvalidInput('username', cherrypy.request.login,
91 'Not in admin group %s.' % config.adminacl)
92 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
95 return super(InvirtWeb, self).__getattr__(name)
97 def handle_error(self):
98 err = sys.exc_info()[1]
99 if isinstance(err, InvalidInput):
100 cherrypy.request.params['err'] = err
101 cherrypy.request.params['emsg'] = revertStandardError()
102 raise cherrypy.InternalRedirect('/invalidInput')
103 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
104 cherrypy.request.params['err'] = err
105 cherrypy.request.params['emsg'] = revertStandardError()
106 cherrypy.request.params['traceback'] = _cperror.format_exc()
107 raise cherrypy.InternalRedirect('/error')
108 # fall back to cherrypy default error page
109 cherrypy.HTTPError(500).set_response()
112 @cherrypy.tools.mako(filename="/list.mako")
113 def list(self, result=None):
114 """Handler for list requests."""
115 checkpoint.checkpoint('Getting list dict')
116 d = getListDict(cherrypy.request.login, cherrypy.request.state)
117 if result is not None:
119 checkpoint.checkpoint('Got list dict')
124 @cherrypy.tools.mako(filename="/help.mako")
125 def help(self, subject=None, simple=False):
126 """Handler for help messages."""
130 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
131 ParaVM. You can access the resulting system by logging into the <a
132 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
133 with your Kerberos tickets; there is no root password so sshd will
136 <p>Under the covers, the autoinstaller uses our own patched version of
137 xen-create-image, which is a tool based on debootstrap. If you log
138 into the serial console while the install is running, you can watch
141 'ParaVM Console': """
142 ParaVM machines do not support local console access over VNC. To
143 access the serial console of these machines, you can SSH with Kerberos
144 to %s, using the name of the machine as your
145 username.""" % config.console.hostname,
147 HVM machines use the virtualization features of the processor, while
148 ParaVM machines rely on a modified kernel to communicate directly with
149 the hypervisor. HVMs support boot CDs of any operating system, and
150 the VNC console applet. The three-minute autoinstaller produces
151 ParaVMs. ParaVMs typically are more efficient, and always support the
152 <a href="help?subject=ParaVM+Console">console server</a>.</p>
154 <p>More details are <a
155 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
156 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
157 (which you can skip by using the autoinstaller to begin with.)</p>
159 <p>We recommend using a ParaVM when possible and an HVM when necessary.
162 Don't ask us! We're as mystified as you are.""",
164 The owner field is used to determine <a
165 href="help?subject=Quotas">quotas</a>. It must be the name of a
166 locker that you are an AFS administrator of. In particular, you or an
167 AFS group you are a member of must have AFS rlidwka bits on the
168 locker. You can check who administers the LOCKER locker using the
169 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
170 href="help?subject=Administrator">administrator</a>.""",
172 The administrator field determines who can access the console and
173 power on and off the machine. This can be either a user or a moira
176 Quotas are determined on a per-locker basis. Each locker may have a
177 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
180 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
181 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
182 your machine will run just fine, but the applet's display of the
183 console will suffer artifacts.
186 <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>
187 <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.
192 subject = sorted(help_mapping.keys())
193 if not isinstance(subject, list):
196 return dict(simple=simple,
198 mapping=help_mapping)
199 help._cp_config['tools.require_login.on'] = False
201 def parseCreate(self, fields):
202 kws = dict([(kw, fields[kw]) for kw in
203 'name description owner memory disksize vmtype cdrom autoinstall'.split()
205 validate = validation.Validate(cherrypy.request.login,
206 cherrypy.request.state,
208 return dict(contact=cherrypy.request.login, name=validate.name,
209 description=validate.description, memory=validate.memory,
210 disksize=validate.disksize, owner=validate.owner,
211 machine_type=getattr(validate, 'vmtype', Defaults.type),
212 cdrom=getattr(validate, 'cdrom', None),
213 autoinstall=getattr(validate, 'autoinstall', None))
216 @cherrypy.tools.mako(filename="/list.mako")
217 @cherrypy.tools.require_POST()
218 def create(self, **fields):
219 """Handler for create requests."""
221 parsed_fields = self.parseCreate(fields)
222 machine = controls.createVm(cherrypy.request.login,
223 cherrypy.request.state, **parsed_fields)
224 except InvalidInput, err:
228 cherrypy.request.state.clear() #Changed global state
229 d = getListDict(cherrypy.request.login, cherrypy.request.state)
232 for field, value in fields.items():
233 setattr(d['defaults'], field, value))
235 d['new_machine'] = parsed_fields['name']
239 @cherrypy.tools.mako(filename="/helloworld.mako")
240 def helloworld(self, **kwargs):
241 return {'request': cherrypy.request, 'kwargs': kwargs}
242 helloworld._cp_config['tools.require_login.on'] = False
246 """Throw an error, to test the error-tracing mechanisms."""
247 print >>sys.stderr, "look ma, it's a stderr"
248 raise RuntimeError("test of the emergency broadcast system")
250 class MachineView(View):
251 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
252 # _cp_dispatch, and parse the argument as a list instead of
255 def __getattr__(self, name):
257 cherrypy.request.params['machine_id'] = int(name)
263 @cherrypy.tools.mako(filename="/info.mako")
264 def info(self, machine_id):
265 """Handler for info on a single VM."""
266 machine = validation.Validate(cherrypy.request.login,
267 cherrypy.request.state,
268 machine_id=machine_id).machine
269 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
270 checkpoint.checkpoint('Got infodict')
275 @cherrypy.tools.mako(filename="/info.mako")
276 @cherrypy.tools.require_POST()
277 def modify(self, machine_id, **fields):
278 """Handler for modifying attributes of a machine."""
280 modify_dict = modifyDict(cherrypy.request.login,
281 cherrypy.request.state,
283 except InvalidInput, err:
285 machine = validation.Validate(cherrypy.request.login,
286 cherrypy.request.state,
287 machine_id=machine_id).machine
289 machine = modify_dict['machine']
292 info_dict = infoDict(cherrypy.request.login,
293 cherrypy.request.state, machine)
294 info_dict['err'] = err
296 for field, value in fields.items():
297 setattr(info_dict['defaults'], field, value)
298 info_dict['result'] = result
302 @cherrypy.tools.mako(filename="/vnc.mako")
303 def vnc(self, machine_id):
306 Note that due to same-domain restrictions, the applet connects to
307 the webserver, which needs to forward those requests to the xen
308 server. The Xen server runs another proxy that (1) authenticates
309 and (2) finds the correct port for the VM.
311 You might want iptables like:
313 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
314 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
315 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
316 --dport 10003 -j SNAT --to-source 18.187.7.142
317 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
318 --dport 10003 -j ACCEPT
320 Remember to enable iptables!
321 echo 1 > /proc/sys/net/ipv4/ip_forward
323 machine = validation.Validate(cherrypy.request.login,
324 cherrypy.request.state,
325 machine_id=machine_id).machine
326 token = controls.vnctoken(machine)
327 host = controls.listHost(machine)
329 port = 10003 + [h.hostname for h in config.hosts].index(host)
333 status = controls.statusInfo(machine)
334 has_vnc = hasVnc(status)
339 hostname=cherrypy.request.local.name,
345 @cherrypy.tools.mako(filename="/command.mako")
346 @cherrypy.tools.require_POST()
347 def command(self, command_name, machine_id, **kwargs):
348 """Handler for running commands like boot and delete on a VM."""
349 back = kwargs.get('back')
351 d = controls.commandResult(cherrypy.request.login,
352 cherrypy.request.state,
353 command_name, machine_id, kwargs)
354 if d['command'] == 'Delete VM':
356 except InvalidInput, err:
359 print >> sys.stderr, err
366 cherrypy.request.state.clear() #Changed global state
367 raise cherrypy.InternalRedirect('/list?result=%s'
368 % urllib.quote(result))
370 raise cherrypy.HTTPRedirect(cherrypy.request.base
371 + '/machine/%d/' % machine_id,
374 raise InvalidInput('back', back, 'Not a known back page.')
376 machine = MachineView()
380 self.start_time = time.time()
381 self.checkpoints = []
383 def checkpoint(self, s):
384 self.checkpoints.append((s, time.time()))
387 return ('Timing info:\n%s\n' %
388 '\n'.join(['%s: %s' % (d, t - self.start_time) for
389 (d, t) in self.checkpoints]))
391 checkpoint = Checkpoint()
394 """Class to store default values for fields."""
404 def __init__(self, max_memory=None, max_disk=None, **kws):
405 if max_memory is not None:
406 self.memory = min(self.memory, max_memory)
407 if max_disk is not None:
408 self.disk = min(self.disk, max_disk)
410 setattr(self, key, kws[key])
413 """Does the machine with a given status list support VNC?"""
417 if l[0] == 'device' and l[1][0] == 'vfb':
419 return 'location' in d
423 def getListDict(username, state):
424 """Gets the list of local variables used by list.tmpl."""
425 checkpoint.checkpoint('Starting')
426 machines = state.machines
427 checkpoint.checkpoint('Got my machines')
431 xmlist = state.xmlist
432 checkpoint.checkpoint('Got uptimes')
438 m.uptime = xmlist[m]['uptime']
439 if xmlist[m]['console']:
444 has_vnc[m] = "ParaVM"
445 if xmlist[m].get('autoinstall'):
448 installing[m] = False
449 max_memory = validation.maxMemory(username, state)
450 max_disk = validation.maxDisk(username)
451 checkpoint.checkpoint('Got max mem/disk')
452 defaults = Defaults(max_memory=max_memory,
455 checkpoint.checkpoint('Got defaults')
456 def sortkey(machine):
457 return (machine.owner != username, machine.owner, machine.name)
458 machines = sorted(machines, key=sortkey)
459 d = dict(user=username,
460 cant_add_vm=validation.cantAddVm(username, state),
461 max_memory=max_memory,
466 installing=installing)
469 def getHostname(nic):
470 """Find the hostname associated with a NIC.
472 XXX this should be merged with the similar logic in DNS and DHCP.
475 hostname = nic.hostname
477 hostname = nic.machine.name
483 return hostname + '.' + config.dns.domains[0]
485 def getNicInfo(data_dict, machine):
486 """Helper function for info, get data on nics for a machine.
488 Modifies data_dict to include the relevant data, and returns a list
489 of (key, name) pairs to display "name: data_dict[key]" to the user.
491 data_dict['num_nics'] = len(machine.nics)
492 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
493 ('nic%s_mac', 'NIC %s MAC Addr'),
494 ('nic%s_ip', 'NIC %s IP'),
497 for i in range(len(machine.nics)):
498 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
499 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
500 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
501 data_dict['nic%s_ip' % i] = machine.nics[i].ip
502 if len(machine.nics) == 1:
503 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
506 def getDiskInfo(data_dict, machine):
507 """Helper function for info, get data on disks for a machine.
509 Modifies data_dict to include the relevant data, and returns a list
510 of (key, name) pairs to display "name: data_dict[key]" to the user.
512 data_dict['num_disks'] = len(machine.disks)
513 disk_fields_template = [('%s_size', '%s size')]
515 for disk in machine.disks:
516 name = disk.guest_device_name
517 disk_fields.extend([(x % name, y % name) for x, y in
518 disk_fields_template])
519 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
522 def modifyDict(username, state, machine_id, fields):
523 """Modify a machine as specified by CGI arguments.
525 Return a dict containing the machine that was modified.
530 kws = dict([(kw, fields[kw]) for kw in
531 'owner admin contact name description memory vmtype disksize'.split()
533 kws['machine_id'] = machine_id
534 validate = validation.Validate(username, state, **kws)
535 machine = validate.machine
536 oldname = machine.name
538 if hasattr(validate, 'memory'):
539 machine.memory = validate.memory
541 if hasattr(validate, 'vmtype'):
542 machine.type = validate.vmtype
544 if hasattr(validate, 'disksize'):
545 disksize = validate.disksize
546 disk = machine.disks[0]
547 if disk.size != disksize:
548 olddisk[disk.guest_device_name] = disksize
550 session.save_or_update(disk)
553 if hasattr(validate, 'owner') and validate.owner != machine.owner:
554 machine.owner = validate.owner
556 if hasattr(validate, 'name'):
557 machine.name = validate.name
558 for n in machine.nics:
559 if n.hostname == oldname:
560 n.hostname = validate.name
561 if hasattr(validate, 'description'):
562 machine.description = validate.description
563 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
564 machine.administrator = validate.admin
566 if hasattr(validate, 'contact'):
567 machine.contact = validate.contact
569 session.save_or_update(machine)
571 cache_acls.refreshMachine(machine)
576 for diskname in olddisk:
577 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
578 if hasattr(validate, 'name'):
579 controls.renameMachine(machine, oldname, validate.name)
580 return dict(machine=machine)
582 def infoDict(username, state, machine):
583 """Get the variables used by info.tmpl."""
584 status = controls.statusInfo(machine)
585 checkpoint.checkpoint('Getting status info')
586 has_vnc = hasVnc(status)
588 main_status = dict(name=machine.name,
589 memory=str(machine.memory))
593 main_status = dict(status[1:])
594 main_status['host'] = controls.listHost(machine)
595 start_time = float(main_status.get('start_time', 0))
596 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
597 cpu_time_float = float(main_status.get('cpu_time', 0))
598 cputime = datetime.timedelta(seconds=int(cpu_time_float))
599 checkpoint.checkpoint('Status')
600 display_fields = [('name', 'Name'),
601 ('description', 'Description'),
603 ('administrator', 'Administrator'),
604 ('contact', 'Contact'),
607 ('uptime', 'uptime'),
608 ('cputime', 'CPU usage'),
609 ('host', 'Hosted on'),
612 ('state', 'state (xen format)'),
616 machine_info['name'] = machine.name
617 machine_info['description'] = machine.description
618 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
619 machine_info['owner'] = machine.owner
620 machine_info['administrator'] = machine.administrator
621 machine_info['contact'] = machine.contact
623 nic_fields = getNicInfo(machine_info, machine)
624 nic_point = display_fields.index('NIC_INFO')
625 display_fields = (display_fields[:nic_point] + nic_fields +
626 display_fields[nic_point+1:])
628 disk_fields = getDiskInfo(machine_info, machine)
629 disk_point = display_fields.index('DISK_INFO')
630 display_fields = (display_fields[:disk_point] + disk_fields +
631 display_fields[disk_point+1:])
633 main_status['memory'] += ' MiB'
634 for field, disp in display_fields:
635 if field in ('uptime', 'cputime') and locals()[field] is not None:
636 fields.append((disp, locals()[field]))
637 elif field in machine_info:
638 fields.append((disp, machine_info[field]))
639 elif field in main_status:
640 fields.append((disp, main_status[field]))
643 #fields.append((disp, None))
645 checkpoint.checkpoint('Got fields')
648 max_mem = validation.maxMemory(machine.owner, state, machine, False)
649 checkpoint.checkpoint('Got mem')
650 max_disk = validation.maxDisk(machine.owner, machine)
651 defaults = Defaults()
652 for name in 'machine_id name description administrator owner memory contact'.split():
653 if getattr(machine, name):
654 setattr(defaults, name, getattr(machine, name))
655 defaults.type = machine.type.type_id
656 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
657 checkpoint.checkpoint('Got defaults')
658 d = dict(user=username,
659 on=status is not None,
670 def send_error_mail(subject, body):
673 to = config.web.errormail
679 """ % (to, config.web.hostname, subject, body)
680 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
681 stdin=subprocess.PIPE)