2 """Main CGI script for web interface"""
17 from cherrypy import _cperror
18 from StringIO import StringIO
21 """Revert stderr to stdout, and print the contents of stderr"""
22 if isinstance(sys.stderr, StringIO):
23 print revertStandardError()
25 if __name__ == '__main__':
27 atexit.register(printError)
31 from webcommon import State
33 from getafsgroups import getAfsGroupMembers
34 from invirt import database
35 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
36 from invirt.config import structs as config
37 from invirt.common import InvalidInput, CodeError
39 from view import View, revertStandardError
42 static_dir = os.path.join(os.path.dirname(__file__), 'static')
43 InvirtStatic = cherrypy.tools.staticdir.handler(
48 class InvirtUnauthWeb(View):
52 @cherrypy.tools.mako(filename="/unauth.mako")
54 return {'simple': True}
56 class InvirtWeb(View):
58 super(self.__class__,self).__init__()
60 self._cp_config['tools.require_login.on'] = True
61 self._cp_config['tools.catch_stderr.on'] = True
62 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
63 'from invirt import database']
64 self._cp_config['request.error_response'] = self.handle_error
69 @cherrypy.tools.mako(filename="/invalid.mako")
70 def invalidInput(self):
71 """Print an error page when an InvalidInput exception occurs"""
72 err = cherrypy.request.prev.params["err"]
73 emsg = cherrypy.request.prev.params["emsg"]
74 d = dict(err_field=err.err_field,
75 err_value=str(err.err_value), stderr=emsg,
76 errorMessage=str(err))
80 @cherrypy.tools.mako(filename="/error.mako")
82 """Print an error page when an exception occurs"""
83 op = cherrypy.request.prev.path_info
84 username = cherrypy.request.login
85 err = cherrypy.request.prev.params["err"]
86 emsg = cherrypy.request.prev.params["emsg"]
87 traceback = cherrypy.request.prev.params["traceback"]
88 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
89 errorMessage=str(err), stderr=emsg, traceback=traceback)
90 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
91 details = error_raw.render(**d)
92 exclude = config.web.errormail_exclude
93 if username not in exclude and '*' not in exclude:
94 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
96 d['details'] = details
99 def __getattr__(self, name):
100 if name in ("admin", "overlord"):
101 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
102 raise InvalidInput('username', cherrypy.request.login,
103 'Not in admin group %s.' % config.adminacl)
104 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
107 return super(InvirtWeb, self).__getattr__(name)
109 def handle_error(self):
110 err = sys.exc_info()[1]
111 if isinstance(err, InvalidInput):
112 cherrypy.request.params['err'] = err
113 cherrypy.request.params['emsg'] = revertStandardError()
114 raise cherrypy.InternalRedirect('/invalidInput')
115 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
116 cherrypy.request.params['err'] = err
117 cherrypy.request.params['emsg'] = revertStandardError()
118 cherrypy.request.params['traceback'] = _cperror.format_exc()
119 raise cherrypy.InternalRedirect('/error')
120 # fall back to cherrypy default error page
121 cherrypy.HTTPError(500).set_response()
124 @cherrypy.tools.mako(filename="/list.mako")
125 def list(self, result=None):
126 """Handler for list requests."""
127 checkpoint.checkpoint('Getting list dict')
128 d = getListDict(cherrypy.request.login, cherrypy.request.state)
129 if result is not None:
131 checkpoint.checkpoint('Got list dict')
136 @cherrypy.tools.mako(filename="/help.mako")
137 def help(self, subject=None, simple=False):
138 """Handler for help messages."""
142 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
143 ParaVM. You can access the resulting system by logging into the <a
144 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
145 with your Kerberos tickets; there is no root password so sshd will
148 <p>Under the covers, the autoinstaller uses our own patched version of
149 xen-create-image, which is a tool based on debootstrap. If you log
150 into the serial console while the install is running, you can watch
153 'ParaVM Console': """
154 ParaVM machines do not support local console access over VNC. To
155 access the serial console of these machines, you can SSH with Kerberos
156 to %s, using the name of the machine as your
157 username.""" % config.console.hostname,
159 HVM machines use the virtualization features of the processor, while
160 ParaVM machines rely on a modified kernel to communicate directly with
161 the hypervisor. HVMs support boot CDs of any operating system, and
162 the VNC console applet. The three-minute autoinstaller produces
163 ParaVMs. ParaVMs typically are more efficient, and always support the
164 <a href="help?subject=ParaVM+Console">console server</a>.</p>
166 <p>More details are <a
167 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
168 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
169 (which you can skip by using the autoinstaller to begin with.)</p>
171 <p>We recommend using a ParaVM when possible and an HVM when necessary.
174 Don't ask us! We're as mystified as you are.""",
176 The owner field is used to determine <a
177 href="help?subject=Quotas">quotas</a>. It must be the name of a
178 locker that you are an AFS administrator of. In particular, you or an
179 AFS group you are a member of must have AFS rlidwka bits on the
180 locker. You can check who administers the LOCKER locker using the
181 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
182 href="help?subject=Administrator">administrator</a>.""",
184 The administrator field determines who can access the console and
185 power on and off the machine. This can be either a user or a moira
188 Quotas are determined on a per-locker basis. Each locker may have a
189 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
192 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
193 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
194 your machine will run just fine, but the applet's display of the
195 console will suffer artifacts.
198 <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>
199 <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.
204 subject = sorted(help_mapping.keys())
205 if not isinstance(subject, list):
208 return dict(simple=simple,
210 mapping=help_mapping)
211 help._cp_config['tools.require_login.on'] = False
213 def parseCreate(self, fields):
214 kws = dict([(kw, fields[kw]) for kw in
215 'name description owner memory disksize vmtype cdrom autoinstall'.split()
217 validate = validation.Validate(cherrypy.request.login,
218 cherrypy.request.state,
220 return dict(contact=cherrypy.request.login, name=validate.name,
221 description=validate.description, memory=validate.memory,
222 disksize=validate.disksize, owner=validate.owner,
223 machine_type=getattr(validate, 'vmtype', Defaults.type),
224 cdrom=getattr(validate, 'cdrom', None),
225 autoinstall=getattr(validate, 'autoinstall', None))
228 @cherrypy.tools.mako(filename="/list.mako")
229 @cherrypy.tools.require_POST()
230 def create(self, **fields):
231 """Handler for create requests."""
233 parsed_fields = self.parseCreate(fields)
234 machine = controls.createVm(cherrypy.request.login,
235 cherrypy.request.state, **parsed_fields)
236 except InvalidInput, err:
240 cherrypy.request.state.clear() #Changed global state
241 d = getListDict(cherrypy.request.login, cherrypy.request.state)
244 for field, value in fields.items():
245 setattr(d['defaults'], field, value)
247 d['new_machine'] = parsed_fields['name']
251 @cherrypy.tools.mako(filename="/helloworld.mako")
252 def helloworld(self, **kwargs):
253 return {'request': cherrypy.request, 'kwargs': kwargs}
254 helloworld._cp_config['tools.require_login.on'] = False
258 """Throw an error, to test the error-tracing mechanisms."""
259 print >>sys.stderr, "look ma, it's a stderr"
260 raise RuntimeError("test of the emergency broadcast system")
262 class MachineView(View):
263 def __getattr__(self, name):
264 """Synthesize attributes to allow RESTful URLs like
265 /machine/13/info. This is hairy. CherryPy 3.2 adds a
266 method called _cp_dispatch that allows you to explicitly
267 handle URLs that can't be mapped, and it allows you to
268 rewrite the path components and continue processing.
270 This function gets the next path component being resolved
271 as a string. _cp_dispatch will get an array of strings
272 representing any subsequent path components as well."""
275 cherrypy.request.params['machine_id'] = int(name)
281 @cherrypy.tools.mako(filename="/info.mako")
282 def info(self, machine_id):
283 """Handler for info on a single VM."""
284 machine = validation.Validate(cherrypy.request.login,
285 cherrypy.request.state,
286 machine_id=machine_id).machine
287 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
288 checkpoint.checkpoint('Got infodict')
293 @cherrypy.tools.mako(filename="/info.mako")
294 @cherrypy.tools.require_POST()
295 def modify(self, machine_id, **fields):
296 """Handler for modifying attributes of a machine."""
298 modify_dict = modifyDict(cherrypy.request.login,
299 cherrypy.request.state,
301 except InvalidInput, err:
303 machine = validation.Validate(cherrypy.request.login,
304 cherrypy.request.state,
305 machine_id=machine_id).machine
307 machine = modify_dict['machine']
310 info_dict = infoDict(cherrypy.request.login,
311 cherrypy.request.state, machine)
312 info_dict['err'] = err
314 for field, value in fields.items():
315 setattr(info_dict['defaults'], field, value)
316 info_dict['result'] = result
320 @cherrypy.tools.mako(filename="/vnc.mako")
321 def vnc(self, machine_id):
324 Note that due to same-domain restrictions, the applet connects to
325 the webserver, which needs to forward those requests to the xen
326 server. The Xen server runs another proxy that (1) authenticates
327 and (2) finds the correct port for the VM.
329 You might want iptables like:
331 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
332 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
333 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
334 --dport 10003 -j SNAT --to-source 18.187.7.142
335 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
336 --dport 10003 -j ACCEPT
338 Remember to enable iptables!
339 echo 1 > /proc/sys/net/ipv4/ip_forward
341 machine = validation.Validate(cherrypy.request.login,
342 cherrypy.request.state,
343 machine_id=machine_id).machine
344 token = controls.vnctoken(machine)
345 host = controls.listHost(machine)
347 port = 10003 + [h.hostname for h in config.hosts].index(host)
351 status = controls.statusInfo(machine)
352 has_vnc = hasVnc(status)
357 hostname=cherrypy.request.local.name,
363 @cherrypy.tools.mako(filename="/command.mako")
364 @cherrypy.tools.require_POST()
365 def command(self, command_name, machine_id, **kwargs):
366 """Handler for running commands like boot and delete on a VM."""
367 back = kwargs.get('back')
368 if command_name == 'delete':
371 d = controls.commandResult(cherrypy.request.login,
372 cherrypy.request.state,
373 command_name, machine_id, kwargs)
374 except InvalidInput, err:
377 print >> sys.stderr, err
384 cherrypy.request.state.clear() #Changed global state
385 raise cherrypy.InternalRedirect('/list?result=%s'
386 % urllib.quote(result))
388 raise cherrypy.HTTPRedirect(cherrypy.request.base
389 + '/machine/%d/' % machine_id,
392 raise InvalidInput('back', back, 'Not a known back page.')
394 machine = MachineView()
398 self.start_time = time.time()
399 self.checkpoints = []
401 def checkpoint(self, s):
402 self.checkpoints.append((s, time.time()))
405 return ('Timing info:\n%s\n' %
406 '\n'.join(['%s: %s' % (d, t - self.start_time) for
407 (d, t) in self.checkpoints]))
409 checkpoint = Checkpoint()
412 """Class to store default values for fields."""
422 def __init__(self, max_memory=None, max_disk=None, **kws):
423 if max_memory is not None:
424 self.memory = min(self.memory, max_memory)
425 if max_disk is not None:
426 self.disk = min(self.disk, max_disk)
428 setattr(self, key, kws[key])
431 """Does the machine with a given status list support VNC?"""
435 if l[0] == 'device' and l[1][0] == 'vfb':
437 return 'location' in d
441 def getListDict(username, state):
442 """Gets the list of local variables used by list.tmpl."""
443 checkpoint.checkpoint('Starting')
444 machines = state.machines
445 checkpoint.checkpoint('Got my machines')
449 xmlist = state.xmlist
450 checkpoint.checkpoint('Got uptimes')
456 m.uptime = xmlist[m]['uptime']
457 installing[m] = bool(xmlist[m].get('autoinstall'))
458 if xmlist[m]['console']:
463 has_vnc[m] = "ParaVM"
464 max_memory = validation.maxMemory(username, state)
465 max_disk = validation.maxDisk(username)
466 checkpoint.checkpoint('Got max mem/disk')
467 defaults = Defaults(max_memory=max_memory,
470 checkpoint.checkpoint('Got defaults')
471 def sortkey(machine):
472 return (machine.owner != username, machine.owner, machine.name)
473 machines = sorted(machines, key=sortkey)
474 d = dict(user=username,
475 cant_add_vm=validation.cantAddVm(username, state),
476 max_memory=max_memory,
481 installing=installing)
484 def getHostname(nic):
485 """Find the hostname associated with a NIC.
487 XXX this should be merged with the similar logic in DNS and DHCP.
490 hostname = nic.hostname
492 hostname = nic.machine.name
498 return hostname + '.' + config.dns.domains[0]
500 def getNicInfo(data_dict, machine):
501 """Helper function for info, get data on nics for a machine.
503 Modifies data_dict to include the relevant data, and returns a list
504 of (key, name) pairs to display "name: data_dict[key]" to the user.
506 data_dict['num_nics'] = len(machine.nics)
507 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
508 ('nic%s_mac', 'NIC %s MAC Addr'),
509 ('nic%s_ip', 'NIC %s IP'),
512 for i in range(len(machine.nics)):
513 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
514 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
515 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
516 data_dict['nic%s_ip' % i] = machine.nics[i].ip
517 if len(machine.nics) == 1:
518 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
521 def getDiskInfo(data_dict, machine):
522 """Helper function for info, get data on disks for a machine.
524 Modifies data_dict to include the relevant data, and returns a list
525 of (key, name) pairs to display "name: data_dict[key]" to the user.
527 data_dict['num_disks'] = len(machine.disks)
528 disk_fields_template = [('%s_size', '%s size')]
530 for disk in machine.disks:
531 name = disk.guest_device_name
532 disk_fields.extend([(x % name, y % name) for x, y in
533 disk_fields_template])
534 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
537 def modifyDict(username, state, machine_id, fields):
538 """Modify a machine as specified by CGI arguments.
540 Return a dict containing the machine that was modified.
545 kws = dict([(kw, fields[kw]) for kw in
546 'owner admin contact name description memory vmtype disksize'.split()
548 kws['machine_id'] = machine_id
549 validate = validation.Validate(username, state, **kws)
550 machine = validate.machine
551 oldname = machine.name
553 if hasattr(validate, 'memory'):
554 machine.memory = validate.memory
556 if hasattr(validate, 'vmtype'):
557 machine.type = validate.vmtype
559 if hasattr(validate, 'disksize'):
560 disksize = validate.disksize
561 disk = machine.disks[0]
562 if disk.size != disksize:
563 olddisk[disk.guest_device_name] = disksize
565 session.save_or_update(disk)
568 if hasattr(validate, 'owner') and validate.owner != machine.owner:
569 machine.owner = validate.owner
571 if hasattr(validate, 'name'):
572 machine.name = validate.name
573 for n in machine.nics:
574 if n.hostname == oldname:
575 n.hostname = validate.name
576 if hasattr(validate, 'description'):
577 machine.description = validate.description
578 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
579 machine.administrator = validate.admin
581 if hasattr(validate, 'contact'):
582 machine.contact = validate.contact
584 session.save_or_update(machine)
586 cache_acls.refreshMachine(machine)
591 for diskname in olddisk:
592 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
593 if hasattr(validate, 'name'):
594 controls.renameMachine(machine, oldname, validate.name)
595 return dict(machine=machine)
597 def infoDict(username, state, machine):
598 """Get the variables used by info.tmpl."""
599 status = controls.statusInfo(machine)
600 checkpoint.checkpoint('Getting status info')
601 has_vnc = hasVnc(status)
603 main_status = dict(name=machine.name,
604 memory=str(machine.memory))
608 main_status = dict(status[1:])
609 main_status['host'] = controls.listHost(machine)
610 start_time = float(main_status.get('start_time', 0))
611 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
612 cpu_time_float = float(main_status.get('cpu_time', 0))
613 cputime = datetime.timedelta(seconds=int(cpu_time_float))
614 checkpoint.checkpoint('Status')
615 display_fields = [('name', 'Name'),
616 ('description', 'Description'),
618 ('administrator', 'Administrator'),
619 ('contact', 'Contact'),
622 ('uptime', 'uptime'),
623 ('cputime', 'CPU usage'),
624 ('host', 'Hosted on'),
627 ('state', 'state (xen format)'),
631 machine_info['name'] = machine.name
632 machine_info['description'] = machine.description
633 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
634 machine_info['owner'] = machine.owner
635 machine_info['administrator'] = machine.administrator
636 machine_info['contact'] = machine.contact
638 nic_fields = getNicInfo(machine_info, machine)
639 nic_point = display_fields.index('NIC_INFO')
640 display_fields = (display_fields[:nic_point] + nic_fields +
641 display_fields[nic_point+1:])
643 disk_fields = getDiskInfo(machine_info, machine)
644 disk_point = display_fields.index('DISK_INFO')
645 display_fields = (display_fields[:disk_point] + disk_fields +
646 display_fields[disk_point+1:])
648 main_status['memory'] += ' MiB'
649 for field, disp in display_fields:
650 if field in ('uptime', 'cputime') and locals()[field] is not None:
651 fields.append((disp, locals()[field]))
652 elif field in machine_info:
653 fields.append((disp, machine_info[field]))
654 elif field in main_status:
655 fields.append((disp, main_status[field]))
658 #fields.append((disp, None))
660 checkpoint.checkpoint('Got fields')
663 max_mem = validation.maxMemory(machine.owner, state, machine, False)
664 checkpoint.checkpoint('Got mem')
665 max_disk = validation.maxDisk(machine.owner, machine)
666 defaults = Defaults()
667 for name in 'machine_id name description administrator owner memory contact'.split():
668 if getattr(machine, name):
669 setattr(defaults, name, getattr(machine, name))
670 defaults.type = machine.type.type_id
671 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
672 checkpoint.checkpoint('Got defaults')
673 d = dict(user=username,
674 on=status is not None,
685 def send_error_mail(subject, body):
688 to = config.web.errormail
694 """ % (to, config.web.hostname, subject, body)
695 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
696 stdin=subprocess.PIPE)