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.afs.cells[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 def __getattr__(self, name):
252 """Synthesize attributes to allow RESTful URLs like
253 /machine/13/info. This is hairy. CherryPy 3.2 adds a
254 method called _cp_dispatch that allows you to explicitly
255 handle URLs that can't be mapped, and it allows you to
256 rewrite the path components and continue processing.
258 This function gets the next path component being resolved
259 as a string. _cp_dispatch will get an array of strings
260 representing any subsequent path components as well."""
263 cherrypy.request.params['machine_id'] = int(name)
269 @cherrypy.tools.mako(filename="/info.mako")
270 def info(self, machine_id):
271 """Handler for info on a single VM."""
272 machine = validation.Validate(cherrypy.request.login,
273 cherrypy.request.state,
274 machine_id=machine_id).machine
275 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
276 checkpoint.checkpoint('Got infodict')
281 @cherrypy.tools.mako(filename="/info.mako")
282 @cherrypy.tools.require_POST()
283 def modify(self, machine_id, **fields):
284 """Handler for modifying attributes of a machine."""
286 modify_dict = modifyDict(cherrypy.request.login,
287 cherrypy.request.state,
289 except InvalidInput, err:
291 machine = validation.Validate(cherrypy.request.login,
292 cherrypy.request.state,
293 machine_id=machine_id).machine
295 machine = modify_dict['machine']
298 info_dict = infoDict(cherrypy.request.login,
299 cherrypy.request.state, machine)
300 info_dict['err'] = err
302 for field, value in fields.items():
303 setattr(info_dict['defaults'], field, value)
304 info_dict['result'] = result
308 @cherrypy.tools.mako(filename="/vnc.mako")
309 def vnc(self, machine_id):
312 Note that due to same-domain restrictions, the applet connects to
313 the webserver, which needs to forward those requests to the xen
314 server. The Xen server runs another proxy that (1) authenticates
315 and (2) finds the correct port for the VM.
317 You might want iptables like:
319 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
320 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
321 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
322 --dport 10003 -j SNAT --to-source 18.187.7.142
323 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
324 --dport 10003 -j ACCEPT
326 Remember to enable iptables!
327 echo 1 > /proc/sys/net/ipv4/ip_forward
329 machine = validation.Validate(cherrypy.request.login,
330 cherrypy.request.state,
331 machine_id=machine_id).machine
332 token = controls.vnctoken(machine)
333 host = controls.listHost(machine)
335 port = 10003 + [h.hostname for h in config.hosts].index(host)
339 status = controls.statusInfo(machine)
340 has_vnc = hasVnc(status)
345 hostname=cherrypy.request.local.name,
351 @cherrypy.tools.mako(filename="/command.mako")
352 @cherrypy.tools.require_POST()
353 def command(self, command_name, machine_id, **kwargs):
354 """Handler for running commands like boot and delete on a VM."""
355 back = kwargs.get('back')
357 d = controls.commandResult(cherrypy.request.login,
358 cherrypy.request.state,
359 command_name, machine_id, kwargs)
360 if d['command'] == 'Delete VM':
362 except InvalidInput, err:
365 print >> sys.stderr, err
372 cherrypy.request.state.clear() #Changed global state
373 raise cherrypy.InternalRedirect('/list?result=%s'
374 % urllib.quote(result))
376 raise cherrypy.HTTPRedirect(cherrypy.request.base
377 + '/machine/%d/' % machine_id,
380 raise InvalidInput('back', back, 'Not a known back page.')
382 machine = MachineView()
386 self.start_time = time.time()
387 self.checkpoints = []
389 def checkpoint(self, s):
390 self.checkpoints.append((s, time.time()))
393 return ('Timing info:\n%s\n' %
394 '\n'.join(['%s: %s' % (d, t - self.start_time) for
395 (d, t) in self.checkpoints]))
397 checkpoint = Checkpoint()
400 """Class to store default values for fields."""
410 def __init__(self, max_memory=None, max_disk=None, **kws):
411 if max_memory is not None:
412 self.memory = min(self.memory, max_memory)
413 if max_disk is not None:
414 self.disk = min(self.disk, max_disk)
416 setattr(self, key, kws[key])
419 """Does the machine with a given status list support VNC?"""
423 if l[0] == 'device' and l[1][0] == 'vfb':
425 return 'location' in d
429 def getListDict(username, state):
430 """Gets the list of local variables used by list.tmpl."""
431 checkpoint.checkpoint('Starting')
432 machines = state.machines
433 checkpoint.checkpoint('Got my machines')
437 xmlist = state.xmlist
438 checkpoint.checkpoint('Got uptimes')
444 m.uptime = xmlist[m]['uptime']
445 installing[m] = bool(xmlist[m].get('autoinstall'))
446 if xmlist[m]['console']:
451 has_vnc[m] = "ParaVM"
452 max_memory = validation.maxMemory(username, state)
453 max_disk = validation.maxDisk(username)
454 checkpoint.checkpoint('Got max mem/disk')
455 defaults = Defaults(max_memory=max_memory,
458 checkpoint.checkpoint('Got defaults')
459 def sortkey(machine):
460 return (machine.owner != username, machine.owner, machine.name)
461 machines = sorted(machines, key=sortkey)
462 d = dict(user=username,
463 cant_add_vm=validation.cantAddVm(username, state),
464 max_memory=max_memory,
469 installing=installing)
472 def getHostname(nic):
473 """Find the hostname associated with a NIC.
475 XXX this should be merged with the similar logic in DNS and DHCP.
478 hostname = nic.hostname
480 hostname = nic.machine.name
486 return hostname + '.' + config.dns.domains[0]
488 def getNicInfo(data_dict, machine):
489 """Helper function for info, get data on nics 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_nics'] = len(machine.nics)
495 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
496 ('nic%s_mac', 'NIC %s MAC Addr'),
497 ('nic%s_ip', 'NIC %s IP'),
500 for i in range(len(machine.nics)):
501 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
502 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
503 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
504 data_dict['nic%s_ip' % i] = machine.nics[i].ip
505 if len(machine.nics) == 1:
506 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
509 def getDiskInfo(data_dict, machine):
510 """Helper function for info, get data on disks for a machine.
512 Modifies data_dict to include the relevant data, and returns a list
513 of (key, name) pairs to display "name: data_dict[key]" to the user.
515 data_dict['num_disks'] = len(machine.disks)
516 disk_fields_template = [('%s_size', '%s size')]
518 for disk in machine.disks:
519 name = disk.guest_device_name
520 disk_fields.extend([(x % name, y % name) for x, y in
521 disk_fields_template])
522 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
525 def modifyDict(username, state, machine_id, fields):
526 """Modify a machine as specified by CGI arguments.
528 Return a dict containing the machine that was modified.
533 kws = dict([(kw, fields[kw]) for kw in
534 'owner admin contact name description memory vmtype disksize'.split()
536 kws['machine_id'] = machine_id
537 validate = validation.Validate(username, state, **kws)
538 machine = validate.machine
539 oldname = machine.name
541 if hasattr(validate, 'memory'):
542 machine.memory = validate.memory
544 if hasattr(validate, 'vmtype'):
545 machine.type = validate.vmtype
547 if hasattr(validate, 'disksize'):
548 disksize = validate.disksize
549 disk = machine.disks[0]
550 if disk.size != disksize:
551 olddisk[disk.guest_device_name] = disksize
553 session.save_or_update(disk)
556 if hasattr(validate, 'owner') and validate.owner != machine.owner:
557 machine.owner = validate.owner
559 if hasattr(validate, 'name'):
560 machine.name = validate.name
561 for n in machine.nics:
562 if n.hostname == oldname:
563 n.hostname = validate.name
564 if hasattr(validate, 'description'):
565 machine.description = validate.description
566 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
567 machine.administrator = validate.admin
569 if hasattr(validate, 'contact'):
570 machine.contact = validate.contact
572 session.save_or_update(machine)
574 cache_acls.refreshMachine(machine)
579 for diskname in olddisk:
580 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
581 if hasattr(validate, 'name'):
582 controls.renameMachine(machine, oldname, validate.name)
583 return dict(machine=machine)
585 def infoDict(username, state, machine):
586 """Get the variables used by info.tmpl."""
587 status = controls.statusInfo(machine)
588 checkpoint.checkpoint('Getting status info')
589 has_vnc = hasVnc(status)
591 main_status = dict(name=machine.name,
592 memory=str(machine.memory))
596 main_status = dict(status[1:])
597 main_status['host'] = controls.listHost(machine)
598 start_time = float(main_status.get('start_time', 0))
599 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
600 cpu_time_float = float(main_status.get('cpu_time', 0))
601 cputime = datetime.timedelta(seconds=int(cpu_time_float))
602 checkpoint.checkpoint('Status')
603 display_fields = [('name', 'Name'),
604 ('description', 'Description'),
606 ('administrator', 'Administrator'),
607 ('contact', 'Contact'),
610 ('uptime', 'uptime'),
611 ('cputime', 'CPU usage'),
612 ('host', 'Hosted on'),
615 ('state', 'state (xen format)'),
619 machine_info['name'] = machine.name
620 machine_info['description'] = machine.description
621 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
622 machine_info['owner'] = machine.owner
623 machine_info['administrator'] = machine.administrator
624 machine_info['contact'] = machine.contact
626 nic_fields = getNicInfo(machine_info, machine)
627 nic_point = display_fields.index('NIC_INFO')
628 display_fields = (display_fields[:nic_point] + nic_fields +
629 display_fields[nic_point+1:])
631 disk_fields = getDiskInfo(machine_info, machine)
632 disk_point = display_fields.index('DISK_INFO')
633 display_fields = (display_fields[:disk_point] + disk_fields +
634 display_fields[disk_point+1:])
636 main_status['memory'] += ' MiB'
637 for field, disp in display_fields:
638 if field in ('uptime', 'cputime') and locals()[field] is not None:
639 fields.append((disp, locals()[field]))
640 elif field in machine_info:
641 fields.append((disp, machine_info[field]))
642 elif field in main_status:
643 fields.append((disp, main_status[field]))
646 #fields.append((disp, None))
648 checkpoint.checkpoint('Got fields')
651 max_mem = validation.maxMemory(machine.owner, state, machine, False)
652 checkpoint.checkpoint('Got mem')
653 max_disk = validation.maxDisk(machine.owner, machine)
654 defaults = Defaults()
655 for name in 'machine_id name description administrator owner memory contact'.split():
656 if getattr(machine, name):
657 setattr(defaults, name, getattr(machine, name))
658 defaults.type = machine.type.type_id
659 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
660 checkpoint.checkpoint('Got defaults')
661 d = dict(user=username,
662 on=status is not None,
673 def send_error_mail(subject, body):
676 to = config.web.errormail
682 """ % (to, config.web.hostname, subject, body)
683 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
684 stdin=subprocess.PIPE)