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
43 static_dir = os.path.join(os.path.dirname(__file__), 'static')
44 InvirtStatic = cherrypy.tools.staticdir.handler(
49 class InvirtUnauthWeb(View):
53 @cherrypy.tools.mako(filename="/unauth.mako")
55 return {'simple': True}
57 class InvirtWeb(View):
59 super(self.__class__,self).__init__()
61 self._cp_config['tools.require_login.on'] = True
62 self._cp_config['tools.catch_stderr.on'] = True
63 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
64 'from invirt import database']
65 self._cp_config['request.error_response'] = self.handle_error
70 @cherrypy.tools.mako(filename="/invalid.mako")
71 def invalidInput(self):
72 """Print an error page when an InvalidInput exception occurs"""
73 err = cherrypy.request.prev.params["err"]
74 emsg = cherrypy.request.prev.params["emsg"]
75 d = dict(err_field=err.err_field,
76 err_value=str(err.err_value), stderr=emsg,
77 errorMessage=str(err))
81 @cherrypy.tools.mako(filename="/error.mako")
83 """Print an error page when an exception occurs"""
84 op = cherrypy.request.prev.path_info
85 username = cherrypy.request.login
86 err = cherrypy.request.prev.params["err"]
87 emsg = cherrypy.request.prev.params["emsg"]
88 traceback = cherrypy.request.prev.params["traceback"]
89 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
90 errorMessage=str(err), stderr=emsg, traceback=traceback)
91 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
92 details = error_raw.render(**d)
93 exclude = config.web.errormail_exclude
94 if username not in exclude and '*' not in exclude:
95 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
97 d['details'] = details
100 def __getattr__(self, name):
101 if name in ("admin", "overlord"):
102 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
103 raise InvalidInput('username', cherrypy.request.login,
104 'Not in admin group %s.' % config.adminacl)
105 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
108 return super(InvirtWeb, self).__getattr__(name)
110 def handle_error(self):
111 err = sys.exc_info()[1]
112 if isinstance(err, InvalidInput):
113 cherrypy.request.params['err'] = err
114 cherrypy.request.params['emsg'] = revertStandardError()
115 raise cherrypy.InternalRedirect('/invalidInput')
116 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
117 cherrypy.request.params['err'] = err
118 cherrypy.request.params['emsg'] = revertStandardError()
119 cherrypy.request.params['traceback'] = _cperror.format_exc()
120 raise cherrypy.InternalRedirect('/error')
121 # fall back to cherrypy default error page
122 cherrypy.HTTPError(500).set_response()
125 @cherrypy.tools.mako(filename="/list.mako")
126 def list(self, result=None):
127 """Handler for list requests."""
128 d = getListDict(cherrypy.request.login, cherrypy.request.state)
129 if result is not None:
135 @cherrypy.tools.mako(filename="/help.mako")
136 def help(self, subject=None, simple=False):
137 """Handler for help messages."""
141 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
142 ParaVM. You can access the resulting system by logging into the <a
143 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
144 with your Kerberos tickets; there is no root password so sshd will
147 <p>Under the covers, the autoinstaller uses our own patched version of
148 xen-create-image, which is a tool based on debootstrap. If you log
149 into the serial console while the install is running, you can watch
152 'ParaVM Console': """
153 ParaVM machines do not support local console access over VNC. To
154 access the serial console of these machines, you can SSH with Kerberos
155 to %s, using the name of the machine as your
156 username.""" % config.console.hostname,
158 HVM machines use the virtualization features of the processor, while
159 ParaVM machines rely on a modified kernel to communicate directly with
160 the hypervisor. HVMs support boot CDs of any operating system, and
161 the VNC console applet. The three-minute autoinstaller produces
162 ParaVMs. ParaVMs typically are more efficient, and always support the
163 <a href="help?subject=ParaVM+Console">console server</a>.</p>
165 <p>More details are <a
166 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
167 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
168 (which you can skip by using the autoinstaller to begin with.)</p>
170 <p>We recommend using a ParaVM when possible and an HVM when necessary.
173 Don't ask us! We're as mystified as you are.""",
175 The owner field is used to determine <a
176 href="help?subject=Quotas">quotas</a>. It must be the name of a
177 locker that you are an AFS administrator of. In particular, you or an
178 AFS group you are a member of must have AFS rlidwka bits on the
179 locker. You can check who administers the LOCKER locker using the
180 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
181 href="help?subject=Administrator">administrator</a>.""",
183 The administrator field determines who can access the console and
184 power on and off the machine. This can be either a user or a moira
187 Quotas are determined on a per-locker basis. Each locker may have a
188 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
191 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
192 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
193 your machine will run just fine, but the applet's display of the
194 console will suffer artifacts.
197 <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>
198 <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.
203 subject = sorted(help_mapping.keys())
204 if not isinstance(subject, list):
207 return dict(simple=simple,
209 mapping=help_mapping)
210 help._cp_config['tools.require_login.on'] = False
212 def parseCreate(self, fields):
213 kws = dict([(kw, fields[kw]) for kw in
214 'name description owner memory disksize vmtype cdrom autoinstall'.split()
216 validate = validation.Validate(cherrypy.request.login,
217 cherrypy.request.state,
219 return dict(contact=cherrypy.request.login, name=validate.name,
220 description=validate.description, memory=validate.memory,
221 disksize=validate.disksize, owner=validate.owner,
222 machine_type=getattr(validate, 'vmtype', Defaults.type),
223 cdrom=getattr(validate, 'cdrom', None),
224 autoinstall=getattr(validate, 'autoinstall', None))
227 @cherrypy.tools.mako(filename="/list.mako")
228 @cherrypy.tools.require_POST()
229 def create(self, **fields):
230 """Handler for create requests."""
232 parsed_fields = self.parseCreate(fields)
233 machine = controls.createVm(cherrypy.request.login,
234 cherrypy.request.state, **parsed_fields)
235 except InvalidInput, err:
239 cherrypy.request.state.clear() #Changed global state
240 d = getListDict(cherrypy.request.login, cherrypy.request.state)
243 for field, value in fields.items():
244 setattr(d['defaults'], field, value)
246 d['new_machine'] = parsed_fields['name']
250 @cherrypy.tools.mako(filename="/helloworld.mako")
251 def helloworld(self, **kwargs):
252 return {'request': cherrypy.request, 'kwargs': kwargs}
253 helloworld._cp_config['tools.require_login.on'] = False
257 """Throw an error, to test the error-tracing mechanisms."""
258 print >>sys.stderr, "look ma, it's a stderr"
259 raise RuntimeError("test of the emergency broadcast system")
261 class MachineView(View):
262 def __getattr__(self, name):
263 """Synthesize attributes to allow RESTful URLs like
264 /machine/13/info. This is hairy. CherryPy 3.2 adds a
265 method called _cp_dispatch that allows you to explicitly
266 handle URLs that can't be mapped, and it allows you to
267 rewrite the path components and continue processing.
269 This function gets the next path component being resolved
270 as a string. _cp_dispatch will get an array of strings
271 representing any subsequent path components as well."""
274 cherrypy.request.params['machine_id'] = int(name)
280 @cherrypy.tools.mako(filename="/info.mako")
281 def info(self, machine_id):
282 """Handler for info on a single VM."""
283 machine = validation.Validate(cherrypy.request.login,
284 cherrypy.request.state,
285 machine_id=machine_id).machine
286 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
291 @cherrypy.tools.mako(filename="/info.mako")
292 @cherrypy.tools.require_POST()
293 def modify(self, machine_id, **fields):
294 """Handler for modifying attributes of a machine."""
296 modify_dict = modifyDict(cherrypy.request.login,
297 cherrypy.request.state,
299 except InvalidInput, err:
301 machine = validation.Validate(cherrypy.request.login,
302 cherrypy.request.state,
303 machine_id=machine_id).machine
305 machine = modify_dict['machine']
308 info_dict = infoDict(cherrypy.request.login,
309 cherrypy.request.state, machine)
310 info_dict['err'] = err
312 for field, value in fields.items():
313 setattr(info_dict['defaults'], field, value)
314 info_dict['result'] = result
318 @cherrypy.tools.mako(filename="/vnc.mako")
319 def vnc(self, machine_id):
322 Note that due to same-domain restrictions, the applet connects to
323 the webserver, which needs to forward those requests to the xen
324 server. The Xen server runs another proxy that (1) authenticates
325 and (2) finds the correct port for the VM.
327 You might want iptables like:
329 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
330 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
331 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
332 --dport 10003 -j SNAT --to-source 18.187.7.142
333 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
334 --dport 10003 -j ACCEPT
336 Remember to enable iptables!
337 echo 1 > /proc/sys/net/ipv4/ip_forward
339 machine = validation.Validate(cherrypy.request.login,
340 cherrypy.request.state,
341 machine_id=machine_id).machine
342 token = controls.vnctoken(machine)
343 host = controls.listHost(machine)
345 port = 10003 + [h.hostname for h in config.hosts].index(host)
349 status = controls.statusInfo(machine)
350 has_vnc = hasVnc(status)
355 hostname=cherrypy.request.local.name,
361 @cherrypy.tools.mako(filename="/command.mako")
362 @cherrypy.tools.require_POST()
363 def command(self, command_name, machine_id, **kwargs):
364 """Handler for running commands like boot and delete on a VM."""
365 back = kwargs.get('back')
366 if command_name == 'delete':
369 d = controls.commandResult(cherrypy.request.login,
370 cherrypy.request.state,
371 command_name, machine_id, kwargs)
372 except InvalidInput, err:
375 print >> sys.stderr, err
382 cherrypy.request.state.clear() #Changed global state
383 raise cherrypy.InternalRedirect('/list?result=%s'
384 % urllib.quote(result))
386 raise cherrypy.HTTPRedirect(cherrypy.request.base
387 + '/machine/%d/' % machine_id,
390 raise InvalidInput('back', back, 'Not a known back page.')
392 atmulti = ajaxterm.Multiplex()
396 @cherrypy.tools.mako(filename="/terminal.mako")
397 def terminal(self, machine_id):
398 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
400 status = controls.statusInfo(machine)
401 has_vnc = hasVnc(status)
406 hostname=cherrypy.request.local.name)
410 @cherrypy.tools.gzip()
411 def at(self, machine_id, k=None, c=0, force=0):
412 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
413 if machine_id in self.atsessions:
414 term = self.atsessions[machine_id]
416 print >>sys.stderr, "spawning new session for terminal to ",machine_id
417 term = self.atsessions[machine_id] = self.atmulti.create(
418 ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
421 self.atmulti.proc_write(term,k)
423 dump=self.atmulti.dump(term,c,int(force))
424 cherrypy.response.headers['Content-Type']='text/xml'
425 if isinstance(dump,str):
428 print "Removing session for", machine_id
429 del self.atsessions[machine_id]
430 return '<?xml version="1.0"?><idem></idem>'
432 machine = MachineView()
436 """Class to store default values for fields."""
446 def __init__(self, max_memory=None, max_disk=None, **kws):
447 if max_memory is not None:
448 self.memory = min(self.memory, max_memory)
449 if max_disk is not None:
450 self.disk = min(self.disk, max_disk)
452 setattr(self, key, kws[key])
455 """Does the machine with a given status list support VNC?"""
459 if l[0] == 'device' and l[1][0] == 'vfb':
461 return 'location' in d
465 def getListDict(username, state):
466 """Gets the list of local variables used by list.tmpl."""
467 machines = state.machines
471 xmlist = state.xmlist
477 m.uptime = xmlist[m]['uptime']
478 installing[m] = bool(xmlist[m].get('autoinstall'))
479 if xmlist[m]['console']:
484 has_vnc[m] = "ParaVM"
485 max_memory = validation.maxMemory(username, state)
486 max_disk = validation.maxDisk(username)
487 defaults = Defaults(max_memory=max_memory,
490 def sortkey(machine):
491 return (machine.owner != username, machine.owner, machine.name)
492 machines = sorted(machines, key=sortkey)
493 d = dict(user=username,
494 cant_add_vm=validation.cantAddVm(username, state),
495 max_memory=max_memory,
500 installing=installing)
503 def getHostname(nic):
504 """Find the hostname associated with a NIC.
506 XXX this should be merged with the similar logic in DNS and DHCP.
509 hostname = nic.hostname
511 hostname = nic.machine.name
517 return hostname + '.' + config.dns.domains[0]
519 def getNicInfo(data_dict, machine):
520 """Helper function for info, get data on nics for a machine.
522 Modifies data_dict to include the relevant data, and returns a list
523 of (key, name) pairs to display "name: data_dict[key]" to the user.
525 data_dict['num_nics'] = len(machine.nics)
526 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
527 ('nic%s_mac', 'NIC %s MAC Addr'),
528 ('nic%s_ip', 'NIC %s IP'),
531 for i in range(len(machine.nics)):
532 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
533 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
534 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
535 data_dict['nic%s_ip' % i] = machine.nics[i].ip
536 if len(machine.nics) == 1:
537 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
540 def getDiskInfo(data_dict, machine):
541 """Helper function for info, get data on disks for a machine.
543 Modifies data_dict to include the relevant data, and returns a list
544 of (key, name) pairs to display "name: data_dict[key]" to the user.
546 data_dict['num_disks'] = len(machine.disks)
547 disk_fields_template = [('%s_size', '%s size')]
549 for disk in machine.disks:
550 name = disk.guest_device_name
551 disk_fields.extend([(x % name, y % name) for x, y in
552 disk_fields_template])
553 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
556 def modifyDict(username, state, machine_id, fields):
557 """Modify a machine as specified by CGI arguments.
559 Return a dict containing the machine that was modified.
564 kws = dict([(kw, fields[kw]) for kw in
565 'owner admin contact name description memory vmtype disksize'.split()
567 kws['machine_id'] = machine_id
568 validate = validation.Validate(username, state, **kws)
569 machine = validate.machine
570 oldname = machine.name
572 if hasattr(validate, 'memory'):
573 machine.memory = validate.memory
575 if hasattr(validate, 'vmtype'):
576 machine.type = validate.vmtype
578 if hasattr(validate, 'disksize'):
579 disksize = validate.disksize
580 disk = machine.disks[0]
581 if disk.size != disksize:
582 olddisk[disk.guest_device_name] = disksize
584 session.save_or_update(disk)
587 if hasattr(validate, 'owner') and validate.owner != machine.owner:
588 machine.owner = validate.owner
590 if hasattr(validate, 'name'):
591 machine.name = validate.name
592 for n in machine.nics:
593 if n.hostname == oldname:
594 n.hostname = validate.name
595 if hasattr(validate, 'description'):
596 machine.description = validate.description
597 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
598 machine.administrator = validate.admin
600 if hasattr(validate, 'contact'):
601 machine.contact = validate.contact
603 session.save_or_update(machine)
605 cache_acls.refreshMachine(machine)
610 for diskname in olddisk:
611 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
612 if hasattr(validate, 'name'):
613 controls.renameMachine(machine, oldname, validate.name)
614 return dict(machine=machine)
616 def infoDict(username, state, machine):
617 """Get the variables used by info.tmpl."""
618 status = controls.statusInfo(machine)
619 has_vnc = hasVnc(status)
621 main_status = dict(name=machine.name,
622 memory=str(machine.memory))
626 main_status = dict(status[1:])
627 main_status['host'] = controls.listHost(machine)
628 start_time = float(main_status.get('start_time', 0))
629 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
630 cpu_time_float = float(main_status.get('cpu_time', 0))
631 cputime = datetime.timedelta(seconds=int(cpu_time_float))
632 display_fields = [('name', 'Name'),
633 ('description', 'Description'),
635 ('administrator', 'Administrator'),
636 ('contact', 'Contact'),
639 ('uptime', 'uptime'),
640 ('cputime', 'CPU usage'),
641 ('host', 'Hosted on'),
644 ('state', 'state (xen format)'),
648 machine_info['name'] = machine.name
649 machine_info['description'] = machine.description
650 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
651 machine_info['owner'] = machine.owner
652 machine_info['administrator'] = machine.administrator
653 machine_info['contact'] = machine.contact
655 nic_fields = getNicInfo(machine_info, machine)
656 nic_point = display_fields.index('NIC_INFO')
657 display_fields = (display_fields[:nic_point] + nic_fields +
658 display_fields[nic_point+1:])
660 disk_fields = getDiskInfo(machine_info, machine)
661 disk_point = display_fields.index('DISK_INFO')
662 display_fields = (display_fields[:disk_point] + disk_fields +
663 display_fields[disk_point+1:])
665 main_status['memory'] += ' MiB'
666 for field, disp in display_fields:
667 if field in ('uptime', 'cputime') and locals()[field] is not None:
668 fields.append((disp, locals()[field]))
669 elif field in machine_info:
670 fields.append((disp, machine_info[field]))
671 elif field in main_status:
672 fields.append((disp, main_status[field]))
675 #fields.append((disp, None))
677 max_mem = validation.maxMemory(machine.owner, state, machine, False)
678 max_disk = validation.maxDisk(machine.owner, machine)
679 defaults = Defaults()
680 for name in 'machine_id name description administrator owner memory contact'.split():
681 if getattr(machine, name):
682 setattr(defaults, name, getattr(machine, name))
683 defaults.type = machine.type.type_id
684 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
685 d = dict(user=username,
686 on=status is not None,
697 def send_error_mail(subject, body):
700 to = config.web.errormail
706 """ % (to, config.web.hostname, subject, body)
707 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
708 stdin=subprocess.PIPE)