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.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
203 validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
204 return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
205 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
206 cdrom=getattr(validate, 'cdrom', None),
207 autoinstall=getattr(validate, 'autoinstall', None))
210 @cherrypy.tools.mako(filename="/list.mako")
211 @cherrypy.tools.require_POST()
212 def create(self, **fields):
213 """Handler for create requests."""
215 parsed_fields = self.parseCreate(fields)
216 machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
217 except InvalidInput, err:
221 cherrypy.request.state.clear() #Changed global state
222 d = getListDict(cherrypy.request.login, cherrypy.request.state)
225 for field in fields.keys():
226 setattr(d['defaults'], field, fields.get(field))
228 d['new_machine'] = parsed_fields['name']
232 @cherrypy.tools.mako(filename="/helloworld.mako")
233 def helloworld(self, **kwargs):
234 return {'request': cherrypy.request, 'kwargs': kwargs}
235 helloworld._cp_config['tools.require_login.on'] = False
239 """Throw an error, to test the error-tracing mechanisms."""
240 print >>sys.stderr, "look ma, it's a stderr"
241 raise RuntimeError("test of the emergency broadcast system")
243 class MachineView(View):
244 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
245 # _cp_dispatch, and parse the argument as a list instead of
248 def __getattr__(self, name):
250 machine_id = int(name)
251 cherrypy.request.params['machine_id'] = machine_id
257 @cherrypy.tools.mako(filename="/info.mako")
258 def info(self, machine_id):
259 """Handler for info on a single VM."""
260 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
261 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
262 checkpoint.checkpoint('Got infodict')
267 @cherrypy.tools.mako(filename="/info.mako")
268 @cherrypy.tools.require_POST()
269 def modify(self, machine_id, **fields):
270 """Handler for modifying attributes of a machine."""
272 modify_dict = modifyDict(cherrypy.request.login, cherrypy.request.state, machine_id, fields)
273 except InvalidInput, err:
275 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
277 machine = modify_dict['machine']
280 info_dict = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
281 info_dict['err'] = err
283 for field in fields.keys():
284 setattr(info_dict['defaults'], field, fields.get(field))
285 info_dict['result'] = result
289 @cherrypy.tools.mako(filename="/vnc.mako")
290 def vnc(self, machine_id):
293 Note that due to same-domain restrictions, the applet connects to
294 the webserver, which needs to forward those requests to the xen
295 server. The Xen server runs another proxy that (1) authenticates
296 and (2) finds the correct port for the VM.
298 You might want iptables like:
300 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
301 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
302 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
303 --dport 10003 -j SNAT --to-source 18.187.7.142
304 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
305 --dport 10003 -j ACCEPT
307 Remember to enable iptables!
308 echo 1 > /proc/sys/net/ipv4/ip_forward
310 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
312 token = controls.vnctoken(machine)
313 host = controls.listHost(machine)
315 port = 10003 + [h.hostname for h in config.hosts].index(host)
319 status = controls.statusInfo(machine)
320 has_vnc = hasVnc(status)
325 hostname=cherrypy.request.local.name,
330 @cherrypy.tools.mako(filename="/command.mako")
331 @cherrypy.tools.require_POST()
332 def command(self, command_name, machine_id, **kwargs):
333 """Handler for running commands like boot and delete on a VM."""
334 back = kwargs.get('back', None)
336 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
337 if d['command'] == 'Delete VM':
339 except InvalidInput, err:
342 print >> sys.stderr, err
349 cherrypy.request.state.clear() #Changed global state
350 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
352 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
354 raise InvalidInput('back', back, 'Not a known back page.')
356 machine = MachineView()
360 self.start_time = time.time()
361 self.checkpoints = []
363 def checkpoint(self, s):
364 self.checkpoints.append((s, time.time()))
367 return ('Timing info:\n%s\n' %
368 '\n'.join(['%s: %s' % (d, t - self.start_time) for
369 (d, t) in self.checkpoints]))
371 checkpoint = Checkpoint()
374 """Class to store default values for fields."""
384 def __init__(self, max_memory=None, max_disk=None, **kws):
385 if max_memory is not None:
386 self.memory = min(self.memory, max_memory)
387 if max_disk is not None:
388 self.disk = min(self.disk, max_disk)
390 setattr(self, key, kws[key])
393 """Does the machine with a given status list support VNC?"""
397 if l[0] == 'device' and l[1][0] == 'vfb':
399 return 'location' in d
403 def getListDict(username, state):
404 """Gets the list of local variables used by list.tmpl."""
405 checkpoint.checkpoint('Starting')
406 machines = state.machines
407 checkpoint.checkpoint('Got my machines')
411 xmlist = state.xmlist
412 checkpoint.checkpoint('Got uptimes')
418 m.uptime = xmlist[m]['uptime']
419 if xmlist[m]['console']:
424 has_vnc[m] = "ParaVM"
425 if xmlist[m].get('autoinstall'):
428 installing[m] = False
429 max_memory = validation.maxMemory(username, state)
430 max_disk = validation.maxDisk(username)
431 checkpoint.checkpoint('Got max mem/disk')
432 defaults = Defaults(max_memory=max_memory,
435 checkpoint.checkpoint('Got defaults')
436 def sortkey(machine):
437 return (machine.owner != username, machine.owner, machine.name)
438 machines = sorted(machines, key=sortkey)
439 d = dict(user=username,
440 cant_add_vm=validation.cantAddVm(username, state),
441 max_memory=max_memory,
446 installing=installing)
449 def getHostname(nic):
450 """Find the hostname associated with a NIC.
452 XXX this should be merged with the similar logic in DNS and DHCP.
455 hostname = nic.hostname
457 hostname = nic.machine.name
463 return hostname + '.' + config.dns.domains[0]
465 def getNicInfo(data_dict, machine):
466 """Helper function for info, get data on nics for a machine.
468 Modifies data_dict to include the relevant data, and returns a list
469 of (key, name) pairs to display "name: data_dict[key]" to the user.
471 data_dict['num_nics'] = len(machine.nics)
472 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
473 ('nic%s_mac', 'NIC %s MAC Addr'),
474 ('nic%s_ip', 'NIC %s IP'),
477 for i in range(len(machine.nics)):
478 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
479 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
480 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
481 data_dict['nic%s_ip' % i] = machine.nics[i].ip
482 if len(machine.nics) == 1:
483 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
486 def getDiskInfo(data_dict, machine):
487 """Helper function for info, get data on disks for a machine.
489 Modifies data_dict to include the relevant data, and returns a list
490 of (key, name) pairs to display "name: data_dict[key]" to the user.
492 data_dict['num_disks'] = len(machine.disks)
493 disk_fields_template = [('%s_size', '%s size')]
495 for disk in machine.disks:
496 name = disk.guest_device_name
497 disk_fields.extend([(x % name, y % name) for x, y in
498 disk_fields_template])
499 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
502 def modifyDict(username, state, machine_id, fields):
503 """Modify a machine as specified by CGI arguments.
505 Return a dict containing the machine that was modified.
510 kws = dict([(kw, fields.get(kw)) for kw in 'owner admin contact name description memory vmtype disksize'.split() if fields.get(kw)])
511 kws['machine_id'] = machine_id
512 validate = validation.Validate(username, state, **kws)
513 machine = validate.machine
514 oldname = machine.name
516 if hasattr(validate, 'memory'):
517 machine.memory = validate.memory
519 if hasattr(validate, 'vmtype'):
520 machine.type = validate.vmtype
522 if hasattr(validate, 'disksize'):
523 disksize = validate.disksize
524 disk = machine.disks[0]
525 if disk.size != disksize:
526 olddisk[disk.guest_device_name] = disksize
528 session.save_or_update(disk)
531 if hasattr(validate, 'owner') and validate.owner != machine.owner:
532 machine.owner = validate.owner
534 if hasattr(validate, 'name'):
535 machine.name = validate.name
536 for n in machine.nics:
537 if n.hostname == oldname:
538 n.hostname = validate.name
539 if hasattr(validate, 'description'):
540 machine.description = validate.description
541 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
542 machine.administrator = validate.admin
544 if hasattr(validate, 'contact'):
545 machine.contact = validate.contact
547 session.save_or_update(machine)
549 cache_acls.refreshMachine(machine)
554 for diskname in olddisk:
555 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
556 if hasattr(validate, 'name'):
557 controls.renameMachine(machine, oldname, validate.name)
558 return dict(machine=machine)
560 def infoDict(username, state, machine):
561 """Get the variables used by info.tmpl."""
562 status = controls.statusInfo(machine)
563 checkpoint.checkpoint('Getting status info')
564 has_vnc = hasVnc(status)
566 main_status = dict(name=machine.name,
567 memory=str(machine.memory))
571 main_status = dict(status[1:])
572 main_status['host'] = controls.listHost(machine)
573 start_time = float(main_status.get('start_time', 0))
574 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
575 cpu_time_float = float(main_status.get('cpu_time', 0))
576 cputime = datetime.timedelta(seconds=int(cpu_time_float))
577 checkpoint.checkpoint('Status')
578 display_fields = [('name', 'Name'),
579 ('description', 'Description'),
581 ('administrator', 'Administrator'),
582 ('contact', 'Contact'),
585 ('uptime', 'uptime'),
586 ('cputime', 'CPU usage'),
587 ('host', 'Hosted on'),
590 ('state', 'state (xen format)'),
594 machine_info['name'] = machine.name
595 machine_info['description'] = machine.description
596 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
597 machine_info['owner'] = machine.owner
598 machine_info['administrator'] = machine.administrator
599 machine_info['contact'] = machine.contact
601 nic_fields = getNicInfo(machine_info, machine)
602 nic_point = display_fields.index('NIC_INFO')
603 display_fields = (display_fields[:nic_point] + nic_fields +
604 display_fields[nic_point+1:])
606 disk_fields = getDiskInfo(machine_info, machine)
607 disk_point = display_fields.index('DISK_INFO')
608 display_fields = (display_fields[:disk_point] + disk_fields +
609 display_fields[disk_point+1:])
611 main_status['memory'] += ' MiB'
612 for field, disp in display_fields:
613 if field in ('uptime', 'cputime') and locals()[field] is not None:
614 fields.append((disp, locals()[field]))
615 elif field in machine_info:
616 fields.append((disp, machine_info[field]))
617 elif field in main_status:
618 fields.append((disp, main_status[field]))
621 #fields.append((disp, None))
623 checkpoint.checkpoint('Got fields')
626 max_mem = validation.maxMemory(machine.owner, state, machine, False)
627 checkpoint.checkpoint('Got mem')
628 max_disk = validation.maxDisk(machine.owner, machine)
629 defaults = Defaults()
630 for name in 'machine_id name description administrator owner memory contact'.split():
631 if getattr(machine, name):
632 setattr(defaults, name, getattr(machine, name))
633 defaults.type = machine.type.type_id
634 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
635 checkpoint.checkpoint('Got defaults')
636 d = dict(user=username,
637 on=status is not None,
648 def send_error_mail(subject, body):
651 to = config.web.errormail
657 """ % (to, config.web.hostname, subject, body)
658 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
659 stdin=subprocess.PIPE)