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):
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)
423 cherrypy.response.headers['Content-Type']='text/xml'
424 if isinstance(dump,str):
427 del self.atsessions[machine_id]
428 return '<?xml version="1.0"?><idem></idem>'
430 machine = MachineView()
434 """Class to store default values for fields."""
444 def __init__(self, max_memory=None, max_disk=None, **kws):
445 if max_memory is not None:
446 self.memory = min(self.memory, max_memory)
447 if max_disk is not None:
448 self.disk = min(self.disk, max_disk)
450 setattr(self, key, kws[key])
453 """Does the machine with a given status list support VNC?"""
457 if l[0] == 'device' and l[1][0] == 'vfb':
459 return 'location' in d
463 def getListDict(username, state):
464 """Gets the list of local variables used by list.tmpl."""
465 machines = state.machines
469 xmlist = state.xmlist
475 m.uptime = xmlist[m]['uptime']
476 installing[m] = bool(xmlist[m].get('autoinstall'))
477 if xmlist[m]['console']:
482 has_vnc[m] = "ParaVM"
483 max_memory = validation.maxMemory(username, state)
484 max_disk = validation.maxDisk(username)
485 defaults = Defaults(max_memory=max_memory,
488 def sortkey(machine):
489 return (machine.owner != username, machine.owner, machine.name)
490 machines = sorted(machines, key=sortkey)
491 d = dict(user=username,
492 cant_add_vm=validation.cantAddVm(username, state),
493 max_memory=max_memory,
498 installing=installing)
501 def getHostname(nic):
502 """Find the hostname associated with a NIC.
504 XXX this should be merged with the similar logic in DNS and DHCP.
507 hostname = nic.hostname
509 hostname = nic.machine.name
515 return hostname + '.' + config.dns.domains[0]
517 def getNicInfo(data_dict, machine):
518 """Helper function for info, get data on nics for a machine.
520 Modifies data_dict to include the relevant data, and returns a list
521 of (key, name) pairs to display "name: data_dict[key]" to the user.
523 data_dict['num_nics'] = len(machine.nics)
524 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
525 ('nic%s_mac', 'NIC %s MAC Addr'),
526 ('nic%s_ip', 'NIC %s IP'),
529 for i in range(len(machine.nics)):
530 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
531 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
532 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
533 data_dict['nic%s_ip' % i] = machine.nics[i].ip
534 if len(machine.nics) == 1:
535 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
538 def getDiskInfo(data_dict, machine):
539 """Helper function for info, get data on disks for a machine.
541 Modifies data_dict to include the relevant data, and returns a list
542 of (key, name) pairs to display "name: data_dict[key]" to the user.
544 data_dict['num_disks'] = len(machine.disks)
545 disk_fields_template = [('%s_size', '%s size')]
547 for disk in machine.disks:
548 name = disk.guest_device_name
549 disk_fields.extend([(x % name, y % name) for x, y in
550 disk_fields_template])
551 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
554 def modifyDict(username, state, machine_id, fields):
555 """Modify a machine as specified by CGI arguments.
557 Return a dict containing the machine that was modified.
562 kws = dict([(kw, fields[kw]) for kw in
563 'owner admin contact name description memory vmtype disksize'.split()
565 kws['machine_id'] = machine_id
566 validate = validation.Validate(username, state, **kws)
567 machine = validate.machine
568 oldname = machine.name
570 if hasattr(validate, 'memory'):
571 machine.memory = validate.memory
573 if hasattr(validate, 'vmtype'):
574 machine.type = validate.vmtype
576 if hasattr(validate, 'disksize'):
577 disksize = validate.disksize
578 disk = machine.disks[0]
579 if disk.size != disksize:
580 olddisk[disk.guest_device_name] = disksize
582 session.save_or_update(disk)
585 if hasattr(validate, 'owner') and validate.owner != machine.owner:
586 machine.owner = validate.owner
588 if hasattr(validate, 'name'):
589 machine.name = validate.name
590 for n in machine.nics:
591 if n.hostname == oldname:
592 n.hostname = validate.name
593 if hasattr(validate, 'description'):
594 machine.description = validate.description
595 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
596 machine.administrator = validate.admin
598 if hasattr(validate, 'contact'):
599 machine.contact = validate.contact
601 session.save_or_update(machine)
603 cache_acls.refreshMachine(machine)
608 for diskname in olddisk:
609 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
610 if hasattr(validate, 'name'):
611 controls.renameMachine(machine, oldname, validate.name)
612 return dict(machine=machine)
614 def infoDict(username, state, machine):
615 """Get the variables used by info.tmpl."""
616 status = controls.statusInfo(machine)
617 has_vnc = hasVnc(status)
619 main_status = dict(name=machine.name,
620 memory=str(machine.memory))
624 main_status = dict(status[1:])
625 main_status['host'] = controls.listHost(machine)
626 start_time = float(main_status.get('start_time', 0))
627 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
628 cpu_time_float = float(main_status.get('cpu_time', 0))
629 cputime = datetime.timedelta(seconds=int(cpu_time_float))
630 display_fields = [('name', 'Name'),
631 ('description', 'Description'),
633 ('administrator', 'Administrator'),
634 ('contact', 'Contact'),
637 ('uptime', 'uptime'),
638 ('cputime', 'CPU usage'),
639 ('host', 'Hosted on'),
642 ('state', 'state (xen format)'),
646 machine_info['name'] = machine.name
647 machine_info['description'] = machine.description
648 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
649 machine_info['owner'] = machine.owner
650 machine_info['administrator'] = machine.administrator
651 machine_info['contact'] = machine.contact
653 nic_fields = getNicInfo(machine_info, machine)
654 nic_point = display_fields.index('NIC_INFO')
655 display_fields = (display_fields[:nic_point] + nic_fields +
656 display_fields[nic_point+1:])
658 disk_fields = getDiskInfo(machine_info, machine)
659 disk_point = display_fields.index('DISK_INFO')
660 display_fields = (display_fields[:disk_point] + disk_fields +
661 display_fields[disk_point+1:])
663 main_status['memory'] += ' MiB'
664 for field, disp in display_fields:
665 if field in ('uptime', 'cputime') and locals()[field] is not None:
666 fields.append((disp, locals()[field]))
667 elif field in machine_info:
668 fields.append((disp, machine_info[field]))
669 elif field in main_status:
670 fields.append((disp, main_status[field]))
673 #fields.append((disp, None))
675 max_mem = validation.maxMemory(machine.owner, state, machine, False)
676 max_disk = validation.maxDisk(machine.owner, machine)
677 defaults = Defaults()
678 for name in 'machine_id name description administrator owner memory contact'.split():
679 if getattr(machine, name):
680 setattr(defaults, name, getattr(machine, name))
681 defaults.type = machine.type.type_id
682 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
683 d = dict(user=username,
684 on=status is not None,
695 def send_error_mail(subject, body):
698 to = config.web.errormail
704 """ % (to, config.web.hostname, subject, body)
705 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
706 stdin=subprocess.PIPE)