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 e = revertStandardError()
101 cherrypy.request.params['err'] = err
102 cherrypy.request.params['emsg'] = e
103 raise cherrypy.InternalRedirect('/invalidInput')
104 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
105 e = revertStandardError()
106 cherrypy.request.params['err'] = err
107 cherrypy.request.params['emsg'] = e
108 cherrypy.request.params['traceback'] = _cperror.format_exc()
109 raise cherrypy.InternalRedirect('/error')
110 # fall back to cherrypy default error page
111 cherrypy.HTTPError(500).set_response()
114 @cherrypy.tools.mako(filename="/list.mako")
115 def list(self, result=None):
116 """Handler for list requests."""
117 checkpoint.checkpoint('Getting list dict')
118 d = getListDict(cherrypy.request.login, cherrypy.request.state)
119 if result is not None:
121 checkpoint.checkpoint('Got list dict')
126 @cherrypy.tools.mako(filename="/help.mako")
127 def help(self, subject=None, simple=False):
128 """Handler for help messages."""
132 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
133 ParaVM. You can access the resulting system by logging into the <a
134 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
135 with your Kerberos tickets; there is no root password so sshd will
138 <p>Under the covers, the autoinstaller uses our own patched version of
139 xen-create-image, which is a tool based on debootstrap. If you log
140 into the serial console while the install is running, you can watch
143 'ParaVM Console': """
144 ParaVM machines do not support local console access over VNC. To
145 access the serial console of these machines, you can SSH with Kerberos
146 to %s, using the name of the machine as your
147 username.""" % config.console.hostname,
149 HVM machines use the virtualization features of the processor, while
150 ParaVM machines rely on a modified kernel to communicate directly with
151 the hypervisor. HVMs support boot CDs of any operating system, and
152 the VNC console applet. The three-minute autoinstaller produces
153 ParaVMs. ParaVMs typically are more efficient, and always support the
154 <a href="help?subject=ParaVM+Console">console server</a>.</p>
156 <p>More details are <a
157 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
158 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
159 (which you can skip by using the autoinstaller to begin with.)</p>
161 <p>We recommend using a ParaVM when possible and an HVM when necessary.
164 Don't ask us! We're as mystified as you are.""",
166 The owner field is used to determine <a
167 href="help?subject=Quotas">quotas</a>. It must be the name of a
168 locker that you are an AFS administrator of. In particular, you or an
169 AFS group you are a member of must have AFS rlidwka bits on the
170 locker. You can check who administers the LOCKER locker using the
171 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
172 href="help?subject=Administrator">administrator</a>.""",
174 The administrator field determines who can access the console and
175 power on and off the machine. This can be either a user or a moira
178 Quotas are determined on a per-locker basis. Each locker may have a
179 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
182 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
183 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
184 your machine will run just fine, but the applet's display of the
185 console will suffer artifacts.
188 <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>
189 <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.
194 subject = sorted(help_mapping.keys())
195 if not isinstance(subject, list):
198 return dict(simple=simple,
200 mapping=help_mapping)
201 help._cp_config['tools.require_login.on'] = False
203 def parseCreate(self, fields):
204 kws = dict([(kw, fields.get(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split() if fields.get(kw)])
205 validate = validation.Validate(cherrypy.request.login, cherrypy.request.state, strict=True, **kws)
206 return dict(contact=cherrypy.request.login, name=validate.name, description=validate.description, memory=validate.memory,
207 disksize=validate.disksize, owner=validate.owner, machine_type=getattr(validate, 'vmtype', Defaults.type),
208 cdrom=getattr(validate, 'cdrom', None),
209 autoinstall=getattr(validate, 'autoinstall', None))
212 @cherrypy.tools.mako(filename="/list.mako")
213 @cherrypy.tools.require_POST()
214 def create(self, **fields):
215 """Handler for create requests."""
217 parsed_fields = self.parseCreate(fields)
218 machine = controls.createVm(cherrypy.request.login, cherrypy.request.state, **parsed_fields)
219 except InvalidInput, err:
223 cherrypy.request.state.clear() #Changed global state
224 d = getListDict(cherrypy.request.login, cherrypy.request.state)
227 for field in fields.keys():
228 setattr(d['defaults'], field, fields.get(field))
230 d['new_machine'] = parsed_fields['name']
234 @cherrypy.tools.mako(filename="/helloworld.mako")
235 def helloworld(self, **kwargs):
236 return {'request': cherrypy.request, 'kwargs': kwargs}
237 helloworld._cp_config['tools.require_login.on'] = False
241 """Throw an error, to test the error-tracing mechanisms."""
242 print >>sys.stderr, "look ma, it's a stderr"
243 raise RuntimeError("test of the emergency broadcast system")
245 class MachineView(View):
246 # This is hairy. Fix when CherryPy 3.2 is out. (rename to
247 # _cp_dispatch, and parse the argument as a list instead of
250 def __getattr__(self, name):
252 machine_id = int(name)
253 cherrypy.request.params['machine_id'] = machine_id
259 @cherrypy.tools.mako(filename="/info.mako")
260 def info(self, machine_id):
261 """Handler for info on a single VM."""
262 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
263 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
264 checkpoint.checkpoint('Got infodict')
269 @cherrypy.tools.mako(filename="/info.mako")
270 @cherrypy.tools.require_POST()
271 def modify(self, machine_id, **fields):
272 """Handler for modifying attributes of a machine."""
274 modify_dict = modifyDict(cherrypy.request.login, cherrypy.request.state, machine_id, fields)
275 except InvalidInput, err:
277 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
279 machine = modify_dict['machine']
282 info_dict = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
283 info_dict['err'] = err
285 for field in fields.keys():
286 setattr(info_dict['defaults'], field, fields.get(field))
287 info_dict['result'] = result
291 @cherrypy.tools.mako(filename="/vnc.mako")
292 def vnc(self, machine_id):
295 Note that due to same-domain restrictions, the applet connects to
296 the webserver, which needs to forward those requests to the xen
297 server. The Xen server runs another proxy that (1) authenticates
298 and (2) finds the correct port for the VM.
300 You might want iptables like:
302 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
303 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
304 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
305 --dport 10003 -j SNAT --to-source 18.187.7.142
306 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
307 --dport 10003 -j ACCEPT
309 Remember to enable iptables!
310 echo 1 > /proc/sys/net/ipv4/ip_forward
312 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
314 token = controls.vnctoken(machine)
315 host = controls.listHost(machine)
317 port = 10003 + [h.hostname for h in config.hosts].index(host)
321 status = controls.statusInfo(machine)
322 has_vnc = hasVnc(status)
327 hostname=cherrypy.request.local.name,
332 @cherrypy.tools.mako(filename="/command.mako")
333 @cherrypy.tools.require_POST()
334 def command(self, command_name, machine_id, **kwargs):
335 """Handler for running commands like boot and delete on a VM."""
336 back = kwargs.get('back', None)
338 d = controls.commandResult(cherrypy.request.login, cherrypy.request.state, command_name, machine_id, kwargs)
339 if d['command'] == 'Delete VM':
341 except InvalidInput, err:
344 print >> sys.stderr, err
351 cherrypy.request.state.clear() #Changed global state
352 raise cherrypy.InternalRedirect('/list?result=%s' % urllib.quote(result))
354 raise cherrypy.HTTPRedirect(cherrypy.request.base + '/machine/%d/' % machine_id, status=303)
356 raise InvalidInput('back', back, 'Not a known back page.')
358 machine = MachineView()
362 self.start_time = time.time()
363 self.checkpoints = []
365 def checkpoint(self, s):
366 self.checkpoints.append((s, time.time()))
369 return ('Timing info:\n%s\n' %
370 '\n'.join(['%s: %s' % (d, t - self.start_time) for
371 (d, t) in self.checkpoints]))
373 checkpoint = Checkpoint()
376 """Class to store default values for fields."""
386 def __init__(self, max_memory=None, max_disk=None, **kws):
387 if max_memory is not None:
388 self.memory = min(self.memory, max_memory)
389 if max_disk is not None:
390 self.disk = min(self.disk, max_disk)
392 setattr(self, key, kws[key])
395 """Does the machine with a given status list support VNC?"""
399 if l[0] == 'device' and l[1][0] == 'vfb':
401 return 'location' in d
405 def getListDict(username, state):
406 """Gets the list of local variables used by list.tmpl."""
407 checkpoint.checkpoint('Starting')
408 machines = state.machines
409 checkpoint.checkpoint('Got my machines')
413 xmlist = state.xmlist
414 checkpoint.checkpoint('Got uptimes')
420 m.uptime = xmlist[m]['uptime']
421 if xmlist[m]['console']:
426 has_vnc[m] = "ParaVM"
427 if xmlist[m].get('autoinstall'):
430 installing[m] = False
431 max_memory = validation.maxMemory(username, state)
432 max_disk = validation.maxDisk(username)
433 checkpoint.checkpoint('Got max mem/disk')
434 defaults = Defaults(max_memory=max_memory,
437 checkpoint.checkpoint('Got defaults')
438 def sortkey(machine):
439 return (machine.owner != username, machine.owner, machine.name)
440 machines = sorted(machines, key=sortkey)
441 d = dict(user=username,
442 cant_add_vm=validation.cantAddVm(username, state),
443 max_memory=max_memory,
448 installing=installing)
451 def getHostname(nic):
452 """Find the hostname associated with a NIC.
454 XXX this should be merged with the similar logic in DNS and DHCP.
457 hostname = nic.hostname
459 hostname = nic.machine.name
465 return hostname + '.' + config.dns.domains[0]
467 def getNicInfo(data_dict, machine):
468 """Helper function for info, get data on nics for a machine.
470 Modifies data_dict to include the relevant data, and returns a list
471 of (key, name) pairs to display "name: data_dict[key]" to the user.
473 data_dict['num_nics'] = len(machine.nics)
474 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
475 ('nic%s_mac', 'NIC %s MAC Addr'),
476 ('nic%s_ip', 'NIC %s IP'),
479 for i in range(len(machine.nics)):
480 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
481 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
482 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
483 data_dict['nic%s_ip' % i] = machine.nics[i].ip
484 if len(machine.nics) == 1:
485 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
488 def getDiskInfo(data_dict, machine):
489 """Helper function for info, get data on disks for a machine.
491 Modifies data_dict to include the relevant data, and returns a list
492 of (key, name) pairs to display "name: data_dict[key]" to the user.
494 data_dict['num_disks'] = len(machine.disks)
495 disk_fields_template = [('%s_size', '%s size')]
497 for disk in machine.disks:
498 name = disk.guest_device_name
499 disk_fields.extend([(x % name, y % name) for x, y in
500 disk_fields_template])
501 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
504 def modifyDict(username, state, machine_id, fields):
505 """Modify a machine as specified by CGI arguments.
507 Return a dict containing the machine that was modified.
512 kws = dict([(kw, fields.get(kw)) for kw in 'owner admin contact name description memory vmtype disksize'.split() if fields.get(kw)])
513 kws['machine_id'] = machine_id
514 validate = validation.Validate(username, state, **kws)
515 machine = validate.machine
516 oldname = machine.name
518 if hasattr(validate, 'memory'):
519 machine.memory = validate.memory
521 if hasattr(validate, 'vmtype'):
522 machine.type = validate.vmtype
524 if hasattr(validate, 'disksize'):
525 disksize = validate.disksize
526 disk = machine.disks[0]
527 if disk.size != disksize:
528 olddisk[disk.guest_device_name] = disksize
530 session.save_or_update(disk)
533 if hasattr(validate, 'owner') and validate.owner != machine.owner:
534 machine.owner = validate.owner
536 if hasattr(validate, 'name'):
537 machine.name = validate.name
538 for n in machine.nics:
539 if n.hostname == oldname:
540 n.hostname = validate.name
541 if hasattr(validate, 'description'):
542 machine.description = validate.description
543 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
544 machine.administrator = validate.admin
546 if hasattr(validate, 'contact'):
547 machine.contact = validate.contact
549 session.save_or_update(machine)
551 cache_acls.refreshMachine(machine)
556 for diskname in olddisk:
557 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
558 if hasattr(validate, 'name'):
559 controls.renameMachine(machine, oldname, validate.name)
560 return dict(machine=machine)
562 def infoDict(username, state, machine):
563 """Get the variables used by info.tmpl."""
564 status = controls.statusInfo(machine)
565 checkpoint.checkpoint('Getting status info')
566 has_vnc = hasVnc(status)
568 main_status = dict(name=machine.name,
569 memory=str(machine.memory))
573 main_status = dict(status[1:])
574 main_status['host'] = controls.listHost(machine)
575 start_time = float(main_status.get('start_time', 0))
576 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
577 cpu_time_float = float(main_status.get('cpu_time', 0))
578 cputime = datetime.timedelta(seconds=int(cpu_time_float))
579 checkpoint.checkpoint('Status')
580 display_fields = [('name', 'Name'),
581 ('description', 'Description'),
583 ('administrator', 'Administrator'),
584 ('contact', 'Contact'),
587 ('uptime', 'uptime'),
588 ('cputime', 'CPU usage'),
589 ('host', 'Hosted on'),
592 ('state', 'state (xen format)'),
596 machine_info['name'] = machine.name
597 machine_info['description'] = machine.description
598 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
599 machine_info['owner'] = machine.owner
600 machine_info['administrator'] = machine.administrator
601 machine_info['contact'] = machine.contact
603 nic_fields = getNicInfo(machine_info, machine)
604 nic_point = display_fields.index('NIC_INFO')
605 display_fields = (display_fields[:nic_point] + nic_fields +
606 display_fields[nic_point+1:])
608 disk_fields = getDiskInfo(machine_info, machine)
609 disk_point = display_fields.index('DISK_INFO')
610 display_fields = (display_fields[:disk_point] + disk_fields +
611 display_fields[disk_point+1:])
613 main_status['memory'] += ' MiB'
614 for field, disp in display_fields:
615 if field in ('uptime', 'cputime') and locals()[field] is not None:
616 fields.append((disp, locals()[field]))
617 elif field in machine_info:
618 fields.append((disp, machine_info[field]))
619 elif field in main_status:
620 fields.append((disp, main_status[field]))
623 #fields.append((disp, None))
625 checkpoint.checkpoint('Got fields')
628 max_mem = validation.maxMemory(machine.owner, state, machine, False)
629 checkpoint.checkpoint('Got mem')
630 max_disk = validation.maxDisk(machine.owner, machine)
631 defaults = Defaults()
632 for name in 'machine_id name description administrator owner memory contact'.split():
633 if getattr(machine, name):
634 setattr(defaults, name, getattr(machine, name))
635 defaults.type = machine.type.type_id
636 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
637 checkpoint.checkpoint('Got defaults')
638 d = dict(user=username,
639 on=status is not None,
650 def send_error_mail(subject, body):
653 to = config.web.errormail
659 """ % (to, config.web.hostname, subject, body)
660 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
661 stdin=subprocess.PIPE)