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 def at(self, machine_id, k=None, c=0, force=0):
411 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
412 if machine_id in self.atsessions:
413 term = self.atsessions[machine_id]
415 print >>sys.stderr, "spawning new session for terminal to ",machine_id
416 term = self.atsessions[machine_id] = self.atmulti.create(
417 ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
420 self.atmulti.proc_write(term,k)
422 dump=self.atmulti.dump(term,c,int(force))
423 cherrypy.response.headers['Content-Type']='text/xml'
424 if isinstance(dump,str):
427 print "Removing session for", machine_id
428 del self.atsessions[machine_id]
429 return '<?xml version="1.0"?><idem></idem>'
431 machine = MachineView()
435 """Class to store default values for fields."""
445 def __init__(self, max_memory=None, max_disk=None, **kws):
446 if max_memory is not None:
447 self.memory = min(self.memory, max_memory)
448 if max_disk is not None:
449 self.disk = min(self.disk, max_disk)
451 setattr(self, key, kws[key])
454 """Does the machine with a given status list support VNC?"""
458 if l[0] == 'device' and l[1][0] == 'vfb':
460 return 'location' in d
464 def getListDict(username, state):
465 """Gets the list of local variables used by list.tmpl."""
466 machines = state.machines
470 xmlist = state.xmlist
476 m.uptime = xmlist[m]['uptime']
477 installing[m] = bool(xmlist[m].get('autoinstall'))
478 if xmlist[m]['console']:
483 has_vnc[m] = "ParaVM"
484 max_memory = validation.maxMemory(username, state)
485 max_disk = validation.maxDisk(username)
486 defaults = Defaults(max_memory=max_memory,
489 def sortkey(machine):
490 return (machine.owner != username, machine.owner, machine.name)
491 machines = sorted(machines, key=sortkey)
492 d = dict(user=username,
493 cant_add_vm=validation.cantAddVm(username, state),
494 max_memory=max_memory,
499 installing=installing)
502 def getHostname(nic):
503 """Find the hostname associated with a NIC.
505 XXX this should be merged with the similar logic in DNS and DHCP.
508 hostname = nic.hostname
510 hostname = nic.machine.name
516 return hostname + '.' + config.dns.domains[0]
518 def getNicInfo(data_dict, machine):
519 """Helper function for info, get data on nics for a machine.
521 Modifies data_dict to include the relevant data, and returns a list
522 of (key, name) pairs to display "name: data_dict[key]" to the user.
524 data_dict['num_nics'] = len(machine.nics)
525 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
526 ('nic%s_mac', 'NIC %s MAC Addr'),
527 ('nic%s_ip', 'NIC %s IP'),
530 for i in range(len(machine.nics)):
531 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
532 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
533 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
534 data_dict['nic%s_ip' % i] = machine.nics[i].ip
535 if len(machine.nics) == 1:
536 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
539 def getDiskInfo(data_dict, machine):
540 """Helper function for info, get data on disks for a machine.
542 Modifies data_dict to include the relevant data, and returns a list
543 of (key, name) pairs to display "name: data_dict[key]" to the user.
545 data_dict['num_disks'] = len(machine.disks)
546 disk_fields_template = [('%s_size', '%s size')]
548 for disk in machine.disks:
549 name = disk.guest_device_name
550 disk_fields.extend([(x % name, y % name) for x, y in
551 disk_fields_template])
552 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
555 def modifyDict(username, state, machine_id, fields):
556 """Modify a machine as specified by CGI arguments.
558 Return a dict containing the machine that was modified.
563 kws = dict([(kw, fields[kw]) for kw in
564 'owner admin contact name description memory vmtype disksize'.split()
566 kws['machine_id'] = machine_id
567 validate = validation.Validate(username, state, **kws)
568 machine = validate.machine
569 oldname = machine.name
571 if hasattr(validate, 'memory'):
572 machine.memory = validate.memory
574 if hasattr(validate, 'vmtype'):
575 machine.type = validate.vmtype
577 if hasattr(validate, 'disksize'):
578 disksize = validate.disksize
579 disk = machine.disks[0]
580 if disk.size != disksize:
581 olddisk[disk.guest_device_name] = disksize
583 session.save_or_update(disk)
586 if hasattr(validate, 'owner') and validate.owner != machine.owner:
587 machine.owner = validate.owner
589 if hasattr(validate, 'name'):
590 machine.name = validate.name
591 for n in machine.nics:
592 if n.hostname == oldname:
593 n.hostname = validate.name
594 if hasattr(validate, 'description'):
595 machine.description = validate.description
596 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
597 machine.administrator = validate.admin
599 if hasattr(validate, 'contact'):
600 machine.contact = validate.contact
602 session.save_or_update(machine)
604 cache_acls.refreshMachine(machine)
609 for diskname in olddisk:
610 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
611 if hasattr(validate, 'name'):
612 controls.renameMachine(machine, oldname, validate.name)
613 return dict(machine=machine)
615 def infoDict(username, state, machine):
616 """Get the variables used by info.tmpl."""
617 status = controls.statusInfo(machine)
618 has_vnc = hasVnc(status)
620 main_status = dict(name=machine.name,
621 memory=str(machine.memory))
625 main_status = dict(status[1:])
626 main_status['host'] = controls.listHost(machine)
627 start_time = float(main_status.get('start_time', 0))
628 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
629 cpu_time_float = float(main_status.get('cpu_time', 0))
630 cputime = datetime.timedelta(seconds=int(cpu_time_float))
631 display_fields = [('name', 'Name'),
632 ('description', 'Description'),
634 ('administrator', 'Administrator'),
635 ('contact', 'Contact'),
638 ('uptime', 'uptime'),
639 ('cputime', 'CPU usage'),
640 ('host', 'Hosted on'),
643 ('state', 'state (xen format)'),
647 machine_info['name'] = machine.name
648 machine_info['description'] = machine.description
649 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
650 machine_info['owner'] = machine.owner
651 machine_info['administrator'] = machine.administrator
652 machine_info['contact'] = machine.contact
654 nic_fields = getNicInfo(machine_info, machine)
655 nic_point = display_fields.index('NIC_INFO')
656 display_fields = (display_fields[:nic_point] + nic_fields +
657 display_fields[nic_point+1:])
659 disk_fields = getDiskInfo(machine_info, machine)
660 disk_point = display_fields.index('DISK_INFO')
661 display_fields = (display_fields[:disk_point] + disk_fields +
662 display_fields[disk_point+1:])
664 main_status['memory'] += ' MiB'
665 for field, disp in display_fields:
666 if field in ('uptime', 'cputime') and locals()[field] is not None:
667 fields.append((disp, locals()[field]))
668 elif field in machine_info:
669 fields.append((disp, machine_info[field]))
670 elif field in main_status:
671 fields.append((disp, main_status[field]))
674 #fields.append((disp, None))
676 max_mem = validation.maxMemory(machine.owner, state, machine, False)
677 max_disk = validation.maxDisk(machine.owner, machine)
678 defaults = Defaults()
679 for name in 'machine_id name description administrator owner memory contact'.split():
680 if getattr(machine, name):
681 setattr(defaults, name, getattr(machine, name))
682 defaults.type = machine.type.type_id
683 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
684 d = dict(user=username,
685 on=status is not None,
696 def send_error_mail(subject, body):
699 to = config.web.errormail
705 """ % (to, config.web.hostname, subject, body)
706 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
707 stdin=subprocess.PIPE)