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 dict(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 d = getListDict(cherrypy.request.login, cherrypy.request.state)
128 if result is not None:
134 @cherrypy.tools.mako(filename="/help.mako")
135 def help(self, subject=None, simple=False):
136 """Handler for help messages."""
140 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
141 ParaVM. You can access the resulting system by logging into the <a
142 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
143 with your Kerberos tickets; there is no root password so sshd will
146 <p>Under the covers, the autoinstaller uses our own patched version of
147 xen-create-image, which is a tool based on debootstrap. If you log
148 into the serial console while the install is running, you can watch
151 'ParaVM Console': """
152 ParaVM machines do not support local console access over VNC. To
153 access the serial console of these machines, you can SSH with Kerberos
154 to %s, using the name of the machine as your
155 username.""" % config.console.hostname,
157 HVM machines use the virtualization features of the processor, while
158 ParaVM machines rely on a modified kernel to communicate directly with
159 the hypervisor. HVMs support boot CDs of any operating system, and
160 the VNC console applet. The three-minute autoinstaller produces
161 ParaVMs. ParaVMs typically are more efficient, and always support the
162 <a href="help?subject=ParaVM+Console">console server</a>.</p>
164 <p>More details are <a
165 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
166 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
167 (which you can skip by using the autoinstaller to begin with.)</p>
169 <p>We recommend using a ParaVM when possible and an HVM when necessary.
172 Don't ask us! We're as mystified as you are.""",
174 The owner field is used to determine <a
175 href="help?subject=Quotas">quotas</a>. It must be the name of a
176 locker that you are an AFS administrator of. In particular, you or an
177 AFS group you are a member of must have AFS rlidwka bits on the
178 locker. You can check who administers the LOCKER locker using the
179 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
180 href="help?subject=Administrator">administrator</a>.""",
182 The administrator field determines who can access the console and
183 power on and off the machine. This can be either a user or a moira
186 Quotas are determined on a per-locker basis. Each locker may have a
187 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
190 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
191 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
192 your machine will run just fine, but the applet's display of the
193 console will suffer artifacts.
196 <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>
197 <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.
202 subject = sorted(help_mapping.keys())
203 if not isinstance(subject, list):
206 return dict(simple=simple,
208 mapping=help_mapping)
209 help._cp_config['tools.require_login.on'] = False
211 def parseCreate(self, fields):
212 kws = dict([(kw, fields[kw]) for kw in
213 'name description owner memory disksize vmtype cdrom autoinstall'.split()
215 validate = validation.Validate(cherrypy.request.login,
216 cherrypy.request.state,
218 return dict(contact=cherrypy.request.login, name=validate.name,
219 description=validate.description, memory=validate.memory,
220 disksize=validate.disksize, owner=validate.owner,
221 machine_type=getattr(validate, 'vmtype', Defaults.type),
222 cdrom=getattr(validate, 'cdrom', None),
223 autoinstall=getattr(validate, 'autoinstall', None))
226 @cherrypy.tools.mako(filename="/list.mako")
227 @cherrypy.tools.require_POST()
228 def create(self, **fields):
229 """Handler for create requests."""
231 parsed_fields = self.parseCreate(fields)
232 machine = controls.createVm(cherrypy.request.login,
233 cherrypy.request.state, **parsed_fields)
234 except InvalidInput, err:
238 cherrypy.request.state.clear() #Changed global state
239 d = getListDict(cherrypy.request.login, cherrypy.request.state)
242 for field, value in fields.items():
243 setattr(d['defaults'], field, value)
245 d['new_machine'] = parsed_fields['name']
249 @cherrypy.tools.mako(filename="/helloworld.mako")
250 def helloworld(self, **kwargs):
251 return {'request': cherrypy.request, 'kwargs': kwargs}
252 helloworld._cp_config['tools.require_login.on'] = False
256 """Throw an error, to test the error-tracing mechanisms."""
257 print >>sys.stderr, "look ma, it's a stderr"
258 raise RuntimeError("test of the emergency broadcast system")
260 class MachineView(View):
261 def __getattr__(self, name):
262 """Synthesize attributes to allow RESTful URLs like
263 /machine/13/info. This is hairy. CherryPy 3.2 adds a
264 method called _cp_dispatch that allows you to explicitly
265 handle URLs that can't be mapped, and it allows you to
266 rewrite the path components and continue processing.
268 This function gets the next path component being resolved
269 as a string. _cp_dispatch will get an array of strings
270 representing any subsequent path components as well."""
273 cherrypy.request.params['machine_id'] = int(name)
279 @cherrypy.tools.mako(filename="/info.mako")
280 def info(self, machine_id):
281 """Handler for info on a single VM."""
282 machine = validation.Validate(cherrypy.request.login,
283 cherrypy.request.state,
284 machine_id=machine_id).machine
285 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
290 @cherrypy.tools.mako(filename="/info.mako")
291 @cherrypy.tools.require_POST()
292 def modify(self, machine_id, **fields):
293 """Handler for modifying attributes of a machine."""
295 modify_dict = modifyDict(cherrypy.request.login,
296 cherrypy.request.state,
298 except InvalidInput, err:
300 machine = validation.Validate(cherrypy.request.login,
301 cherrypy.request.state,
302 machine_id=machine_id).machine
304 machine = modify_dict['machine']
307 info_dict = infoDict(cherrypy.request.login,
308 cherrypy.request.state, machine)
309 info_dict['err'] = err
311 for field, value in fields.items():
312 setattr(info_dict['defaults'], field, value)
313 info_dict['result'] = result
317 @cherrypy.tools.mako(filename="/vnc.mako")
318 def vnc(self, machine_id):
321 Note that due to same-domain restrictions, the applet connects to
322 the webserver, which needs to forward those requests to the xen
323 server. The Xen server runs another proxy that (1) authenticates
324 and (2) finds the correct port for the VM.
326 You might want iptables like:
328 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
329 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
330 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
331 --dport 10003 -j SNAT --to-source 18.187.7.142
332 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
333 --dport 10003 -j ACCEPT
335 Remember to enable iptables!
336 echo 1 > /proc/sys/net/ipv4/ip_forward
338 machine = validation.Validate(cherrypy.request.login,
339 cherrypy.request.state,
340 machine_id=machine_id).machine
341 token = controls.vnctoken(machine)
342 host = controls.listHost(machine)
344 port = 10003 + [h.hostname for h in config.hosts].index(host)
348 status = controls.statusInfo(machine)
349 has_vnc = hasVnc(status)
354 hostname=cherrypy.request.local.name,
360 @cherrypy.tools.mako(filename="/command.mako")
361 @cherrypy.tools.require_POST()
362 def command(self, command_name, machine_id, **kwargs):
363 """Handler for running commands like boot and delete on a VM."""
364 back = kwargs.get('back')
365 if command_name == 'delete':
368 d = controls.commandResult(cherrypy.request.login,
369 cherrypy.request.state,
370 command_name, machine_id, kwargs)
371 except InvalidInput, err:
374 print >> sys.stderr, err
381 cherrypy.request.state.clear() #Changed global state
382 raise cherrypy.InternalRedirect('/list?result=%s'
383 % urllib.quote(result))
385 raise cherrypy.HTTPRedirect(cherrypy.request.base
386 + '/machine/%d/' % machine_id,
389 raise InvalidInput('back', back, 'Not a known back page.')
391 machine = MachineView()
395 """Class to store default values for fields."""
405 def __init__(self, max_memory=None, max_disk=None, **kws):
406 if max_memory is not None:
407 self.memory = min(self.memory, max_memory)
408 if max_disk is not None:
409 self.disk = min(self.disk, max_disk)
411 setattr(self, key, kws[key])
414 """Does the machine with a given status list support VNC?"""
418 if l[0] == 'device' and l[1][0] == 'vfb':
420 return 'location' in d
424 def getListDict(username, state):
425 """Gets the list of local variables used by list.tmpl."""
426 machines = state.machines
430 xmlist = state.xmlist
436 m.uptime = xmlist[m]['uptime']
437 installing[m] = bool(xmlist[m].get('autoinstall'))
438 if xmlist[m]['console']:
443 has_vnc[m] = "ParaVM"
444 max_memory = validation.maxMemory(username, state)
445 max_disk = validation.maxDisk(username)
446 defaults = Defaults(max_memory=max_memory,
449 def sortkey(machine):
450 return (machine.owner != username, machine.owner, machine.name)
451 machines = sorted(machines, key=sortkey)
452 d = dict(user=username,
453 cant_add_vm=validation.cantAddVm(username, state),
454 max_memory=max_memory,
459 installing=installing)
462 def getHostname(nic):
463 """Find the hostname associated with a NIC.
465 XXX this should be merged with the similar logic in DNS and DHCP.
468 hostname = nic.hostname
470 hostname = nic.machine.name
476 return hostname + '.' + config.dns.domains[0]
478 def getNicInfo(data_dict, machine):
479 """Helper function for info, get data on nics for a machine.
481 Modifies data_dict to include the relevant data, and returns a list
482 of (key, name) pairs to display "name: data_dict[key]" to the user.
484 data_dict['num_nics'] = len(machine.nics)
485 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
486 ('nic%s_mac', 'NIC %s MAC Addr'),
487 ('nic%s_ip', 'NIC %s IP'),
490 for i in range(len(machine.nics)):
491 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
492 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
493 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
494 data_dict['nic%s_ip' % i] = machine.nics[i].ip
495 if len(machine.nics) == 1:
496 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
499 def getDiskInfo(data_dict, machine):
500 """Helper function for info, get data on disks for a machine.
502 Modifies data_dict to include the relevant data, and returns a list
503 of (key, name) pairs to display "name: data_dict[key]" to the user.
505 data_dict['num_disks'] = len(machine.disks)
506 disk_fields_template = [('%s_size', '%s size')]
508 for disk in machine.disks:
509 name = disk.guest_device_name
510 disk_fields.extend([(x % name, y % name) for x, y in
511 disk_fields_template])
512 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
515 def modifyDict(username, state, machine_id, fields):
516 """Modify a machine as specified by CGI arguments.
518 Return a dict containing the machine that was modified.
523 kws = dict((kw, fields[kw]) for kw in
524 'owner admin contact name description memory vmtype disksize'.split()
526 kws['machine_id'] = machine_id
527 validate = validation.Validate(username, state, **kws)
528 machine = validate.machine
529 oldname = machine.name
531 if hasattr(validate, 'memory'):
532 machine.memory = validate.memory
534 if hasattr(validate, 'vmtype'):
535 machine.type = validate.vmtype
537 if hasattr(validate, 'disksize'):
538 disksize = validate.disksize
539 disk = machine.disks[0]
540 if disk.size != disksize:
541 olddisk[disk.guest_device_name] = disksize
543 session.save_or_update(disk)
546 if hasattr(validate, 'owner') and validate.owner != machine.owner:
547 machine.owner = validate.owner
549 if hasattr(validate, 'name'):
550 machine.name = validate.name
551 for n in machine.nics:
552 if n.hostname == oldname:
553 n.hostname = validate.name
554 if hasattr(validate, 'description'):
555 machine.description = validate.description
556 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
557 machine.administrator = validate.admin
559 if hasattr(validate, 'contact'):
560 machine.contact = validate.contact
562 session.save_or_update(machine)
564 cache_acls.refreshMachine(machine)
569 for diskname in olddisk:
570 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
571 if hasattr(validate, 'name'):
572 controls.renameMachine(machine, oldname, validate.name)
573 return dict(machine=machine)
575 def infoDict(username, state, machine):
576 """Get the variables used by info.tmpl."""
577 status = controls.statusInfo(machine)
578 has_vnc = hasVnc(status)
580 main_status = dict(name=machine.name,
581 memory=str(machine.memory))
585 main_status = dict(status[1:])
586 main_status['host'] = controls.listHost(machine)
587 start_time = float(main_status.get('start_time', 0))
588 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
589 cpu_time_float = float(main_status.get('cpu_time', 0))
590 cputime = datetime.timedelta(seconds=int(cpu_time_float))
591 display_fields = [('name', 'Name'),
592 ('description', 'Description'),
594 ('administrator', 'Administrator'),
595 ('contact', 'Contact'),
598 ('uptime', 'uptime'),
599 ('cputime', 'CPU usage'),
600 ('host', 'Hosted on'),
603 ('state', 'state (xen format)'),
607 machine_info['name'] = machine.name
608 machine_info['description'] = machine.description
609 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
610 machine_info['owner'] = machine.owner
611 machine_info['administrator'] = machine.administrator
612 machine_info['contact'] = machine.contact
614 nic_fields = getNicInfo(machine_info, machine)
615 nic_point = display_fields.index('NIC_INFO')
616 display_fields = (display_fields[:nic_point] + nic_fields +
617 display_fields[nic_point+1:])
619 disk_fields = getDiskInfo(machine_info, machine)
620 disk_point = display_fields.index('DISK_INFO')
621 display_fields = (display_fields[:disk_point] + disk_fields +
622 display_fields[disk_point+1:])
624 main_status['memory'] += ' MiB'
625 for field, disp in display_fields:
626 if field in ('uptime', 'cputime') and locals()[field] is not None:
627 fields.append((disp, locals()[field]))
628 elif field in machine_info:
629 fields.append((disp, machine_info[field]))
630 elif field in main_status:
631 fields.append((disp, main_status[field]))
634 #fields.append((disp, None))
636 max_mem = validation.maxMemory(machine.owner, state, machine, False)
637 max_disk = validation.maxDisk(machine.owner, machine)
638 defaults = Defaults()
639 for name in 'machine_id name description administrator owner memory contact'.split():
640 if getattr(machine, name):
641 setattr(defaults, name, getattr(machine, name))
642 defaults.type = machine.type.type_id
643 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
644 d = dict(user=username,
645 on=status is not None,
656 def send_error_mail(subject, body):
659 to = config.web.errormail
665 """ % (to, config.web.hostname, subject, body)
666 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
667 stdin=subprocess.PIPE)