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,
344 @cherrypy.tools.mako(filename="/command.mako")
345 @cherrypy.tools.require_POST()
346 def command(self, command_name, machine_id, **kwargs):
347 """Handler for running commands like boot and delete on a VM."""
348 back = kwargs.get('back', None)
350 d = controls.commandResult(cherrypy.request.login,
351 cherrypy.request.state,
352 command_name, machine_id, kwargs)
353 if d['command'] == 'Delete VM':
355 except InvalidInput, err:
358 print >> sys.stderr, err
365 cherrypy.request.state.clear() #Changed global state
366 raise cherrypy.InternalRedirect('/list?result=%s'
367 % urllib.quote(result))
369 raise cherrypy.HTTPRedirect(cherrypy.request.base
370 + '/machine/%d/' % machine_id,
373 raise InvalidInput('back', back, 'Not a known back page.')
375 machine = MachineView()
379 self.start_time = time.time()
380 self.checkpoints = []
382 def checkpoint(self, s):
383 self.checkpoints.append((s, time.time()))
386 return ('Timing info:\n%s\n' %
387 '\n'.join(['%s: %s' % (d, t - self.start_time) for
388 (d, t) in self.checkpoints]))
390 checkpoint = Checkpoint()
393 """Class to store default values for fields."""
403 def __init__(self, max_memory=None, max_disk=None, **kws):
404 if max_memory is not None:
405 self.memory = min(self.memory, max_memory)
406 if max_disk is not None:
407 self.disk = min(self.disk, max_disk)
409 setattr(self, key, kws[key])
412 """Does the machine with a given status list support VNC?"""
416 if l[0] == 'device' and l[1][0] == 'vfb':
418 return 'location' in d
422 def getListDict(username, state):
423 """Gets the list of local variables used by list.tmpl."""
424 checkpoint.checkpoint('Starting')
425 machines = state.machines
426 checkpoint.checkpoint('Got my machines')
430 xmlist = state.xmlist
431 checkpoint.checkpoint('Got uptimes')
437 m.uptime = xmlist[m]['uptime']
438 if xmlist[m]['console']:
443 has_vnc[m] = "ParaVM"
444 if xmlist[m].get('autoinstall'):
447 installing[m] = False
448 max_memory = validation.maxMemory(username, state)
449 max_disk = validation.maxDisk(username)
450 checkpoint.checkpoint('Got max mem/disk')
451 defaults = Defaults(max_memory=max_memory,
454 checkpoint.checkpoint('Got defaults')
455 def sortkey(machine):
456 return (machine.owner != username, machine.owner, machine.name)
457 machines = sorted(machines, key=sortkey)
458 d = dict(user=username,
459 cant_add_vm=validation.cantAddVm(username, state),
460 max_memory=max_memory,
465 installing=installing)
468 def getHostname(nic):
469 """Find the hostname associated with a NIC.
471 XXX this should be merged with the similar logic in DNS and DHCP.
474 hostname = nic.hostname
476 hostname = nic.machine.name
482 return hostname + '.' + config.dns.domains[0]
484 def getNicInfo(data_dict, machine):
485 """Helper function for info, get data on nics for a machine.
487 Modifies data_dict to include the relevant data, and returns a list
488 of (key, name) pairs to display "name: data_dict[key]" to the user.
490 data_dict['num_nics'] = len(machine.nics)
491 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
492 ('nic%s_mac', 'NIC %s MAC Addr'),
493 ('nic%s_ip', 'NIC %s IP'),
496 for i in range(len(machine.nics)):
497 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
498 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
499 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
500 data_dict['nic%s_ip' % i] = machine.nics[i].ip
501 if len(machine.nics) == 1:
502 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
505 def getDiskInfo(data_dict, machine):
506 """Helper function for info, get data on disks for a machine.
508 Modifies data_dict to include the relevant data, and returns a list
509 of (key, name) pairs to display "name: data_dict[key]" to the user.
511 data_dict['num_disks'] = len(machine.disks)
512 disk_fields_template = [('%s_size', '%s size')]
514 for disk in machine.disks:
515 name = disk.guest_device_name
516 disk_fields.extend([(x % name, y % name) for x, y in
517 disk_fields_template])
518 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
521 def modifyDict(username, state, machine_id, fields):
522 """Modify a machine as specified by CGI arguments.
524 Return a dict containing the machine that was modified.
529 kws = dict([(kw, fields[kw]) for kw in
530 'owner admin contact name description memory vmtype disksize'.split()
532 kws['machine_id'] = machine_id
533 validate = validation.Validate(username, state, **kws)
534 machine = validate.machine
535 oldname = machine.name
537 if hasattr(validate, 'memory'):
538 machine.memory = validate.memory
540 if hasattr(validate, 'vmtype'):
541 machine.type = validate.vmtype
543 if hasattr(validate, 'disksize'):
544 disksize = validate.disksize
545 disk = machine.disks[0]
546 if disk.size != disksize:
547 olddisk[disk.guest_device_name] = disksize
549 session.save_or_update(disk)
552 if hasattr(validate, 'owner') and validate.owner != machine.owner:
553 machine.owner = validate.owner
555 if hasattr(validate, 'name'):
556 machine.name = validate.name
557 for n in machine.nics:
558 if n.hostname == oldname:
559 n.hostname = validate.name
560 if hasattr(validate, 'description'):
561 machine.description = validate.description
562 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
563 machine.administrator = validate.admin
565 if hasattr(validate, 'contact'):
566 machine.contact = validate.contact
568 session.save_or_update(machine)
570 cache_acls.refreshMachine(machine)
575 for diskname in olddisk:
576 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
577 if hasattr(validate, 'name'):
578 controls.renameMachine(machine, oldname, validate.name)
579 return dict(machine=machine)
581 def infoDict(username, state, machine):
582 """Get the variables used by info.tmpl."""
583 status = controls.statusInfo(machine)
584 checkpoint.checkpoint('Getting status info')
585 has_vnc = hasVnc(status)
587 main_status = dict(name=machine.name,
588 memory=str(machine.memory))
592 main_status = dict(status[1:])
593 main_status['host'] = controls.listHost(machine)
594 start_time = float(main_status.get('start_time', 0))
595 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
596 cpu_time_float = float(main_status.get('cpu_time', 0))
597 cputime = datetime.timedelta(seconds=int(cpu_time_float))
598 checkpoint.checkpoint('Status')
599 display_fields = [('name', 'Name'),
600 ('description', 'Description'),
602 ('administrator', 'Administrator'),
603 ('contact', 'Contact'),
606 ('uptime', 'uptime'),
607 ('cputime', 'CPU usage'),
608 ('host', 'Hosted on'),
611 ('state', 'state (xen format)'),
615 machine_info['name'] = machine.name
616 machine_info['description'] = machine.description
617 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
618 machine_info['owner'] = machine.owner
619 machine_info['administrator'] = machine.administrator
620 machine_info['contact'] = machine.contact
622 nic_fields = getNicInfo(machine_info, machine)
623 nic_point = display_fields.index('NIC_INFO')
624 display_fields = (display_fields[:nic_point] + nic_fields +
625 display_fields[nic_point+1:])
627 disk_fields = getDiskInfo(machine_info, machine)
628 disk_point = display_fields.index('DISK_INFO')
629 display_fields = (display_fields[:disk_point] + disk_fields +
630 display_fields[disk_point+1:])
632 main_status['memory'] += ' MiB'
633 for field, disp in display_fields:
634 if field in ('uptime', 'cputime') and locals()[field] is not None:
635 fields.append((disp, locals()[field]))
636 elif field in machine_info:
637 fields.append((disp, machine_info[field]))
638 elif field in main_status:
639 fields.append((disp, main_status[field]))
642 #fields.append((disp, None))
644 checkpoint.checkpoint('Got fields')
647 max_mem = validation.maxMemory(machine.owner, state, machine, False)
648 checkpoint.checkpoint('Got mem')
649 max_disk = validation.maxDisk(machine.owner, machine)
650 defaults = Defaults()
651 for name in 'machine_id name description administrator owner memory contact'.split():
652 if getattr(machine, name):
653 setattr(defaults, name, getattr(machine, name))
654 defaults.type = machine.type.type_id
655 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
656 checkpoint.checkpoint('Got defaults')
657 d = dict(user=username,
658 on=status is not None,
669 def send_error_mail(subject, body):
672 to = config.web.errormail
678 """ % (to, config.web.hostname, subject, body)
679 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
680 stdin=subprocess.PIPE)