2 """Main CGI script for web interface"""
4 from __future__ import with_statement
20 from cherrypy import _cperror
21 from StringIO import StringIO
24 """Revert stderr to stdout, and print the contents of stderr"""
25 if isinstance(sys.stderr, StringIO):
26 print revertStandardError()
28 if __name__ == '__main__':
30 atexit.register(printError)
34 from webcommon import State
36 from getafsgroups import getAfsGroupMembers
37 from invirt import database
38 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
39 from invirt.config import structs as config
40 from invirt.common import InvalidInput, CodeError
42 from view import View, revertStandardError
46 static_dir = os.path.join(os.path.dirname(__file__), 'static')
47 InvirtStatic = cherrypy.tools.staticdir.handler(
52 class InvirtUnauthWeb(View):
56 @cherrypy.tools.mako(filename="/unauth.mako")
58 return {'simple': True}
60 class InvirtWeb(View):
62 super(self.__class__,self).__init__()
64 self._cp_config['tools.require_login.on'] = True
65 self._cp_config['tools.catch_stderr.on'] = True
66 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
67 'from invirt import database']
68 self._cp_config['request.error_response'] = self.handle_error
73 @cherrypy.tools.mako(filename="/invalid.mako")
74 def invalidInput(self):
75 """Print an error page when an InvalidInput exception occurs"""
76 err = cherrypy.request.prev.params["err"]
77 emsg = cherrypy.request.prev.params["emsg"]
78 d = dict(err_field=err.err_field,
79 err_value=str(err.err_value), stderr=emsg,
80 errorMessage=str(err))
84 @cherrypy.tools.mako(filename="/error.mako")
86 """Print an error page when an exception occurs"""
87 op = cherrypy.request.prev.path_info
88 username = cherrypy.request.login
89 err = cherrypy.request.prev.params["err"]
90 emsg = cherrypy.request.prev.params["emsg"]
91 traceback = cherrypy.request.prev.params["traceback"]
92 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
93 errorMessage=str(err), stderr=emsg, traceback=traceback)
94 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
95 details = error_raw.render(**d)
96 exclude = config.web.errormail_exclude
97 if username not in exclude and '*' not in exclude:
98 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
100 d['details'] = details
103 def __getattr__(self, name):
104 if name in ("admin", "overlord"):
105 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
106 raise InvalidInput('username', cherrypy.request.login,
107 'Not in admin group %s.' % config.adminacl)
108 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
111 return super(InvirtWeb, self).__getattr__(name)
113 def handle_error(self):
114 err = sys.exc_info()[1]
115 if isinstance(err, InvalidInput):
116 cherrypy.request.params['err'] = err
117 cherrypy.request.params['emsg'] = revertStandardError()
118 raise cherrypy.InternalRedirect('/invalidInput')
119 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
120 cherrypy.request.params['err'] = err
121 cherrypy.request.params['emsg'] = revertStandardError()
122 cherrypy.request.params['traceback'] = _cperror.format_exc()
123 raise cherrypy.InternalRedirect('/error')
124 # fall back to cherrypy default error page
125 cherrypy.HTTPError(500).set_response()
128 @cherrypy.tools.mako(filename="/list.mako")
129 def list(self, result=None):
130 """Handler for list requests."""
131 d = getListDict(cherrypy.request.login, cherrypy.request.state)
132 if result is not None:
138 @cherrypy.tools.mako(filename="/help.mako")
139 def help(self, subject=None, simple=False):
140 """Handler for help messages."""
144 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
145 ParaVM. You can access the resulting system by logging into the <a
146 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
147 with your Kerberos tickets; there is no root password so sshd will
150 <p>Under the covers, the autoinstaller uses our own patched version of
151 xen-create-image, which is a tool based on debootstrap. If you log
152 into the serial console while the install is running, you can watch
155 'ParaVM Console': """
156 ParaVM machines do not support local console access over VNC. To
157 access the serial console of these machines, you can SSH with Kerberos
158 to %s, using the name of the machine as your
159 username.""" % config.console.hostname,
161 HVM machines use the virtualization features of the processor, while
162 ParaVM machines rely on a modified kernel to communicate directly with
163 the hypervisor. HVMs support boot CDs of any operating system, and
164 the VNC console applet. The three-minute autoinstaller produces
165 ParaVMs. ParaVMs typically are more efficient, and always support the
166 <a href="help?subject=ParaVM+Console">console server</a>.</p>
168 <p>More details are <a
169 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
170 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
171 (which you can skip by using the autoinstaller to begin with.)</p>
173 <p>We recommend using a ParaVM when possible and an HVM when necessary.
176 Don't ask us! We're as mystified as you are.""",
178 The owner field is used to determine <a
179 href="help?subject=Quotas">quotas</a>. It must be the name of a
180 locker that you are an AFS administrator of. In particular, you or an
181 AFS group you are a member of must have AFS rlidwka bits on the
182 locker. You can check who administers the LOCKER locker using the
183 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
184 href="help?subject=Administrator">administrator</a>.""",
186 The administrator field determines who can access the console and
187 power on and off the machine. This can be either a user or a moira
190 Quotas are determined on a per-locker basis. Each locker may have a
191 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
194 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
195 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
196 your machine will run just fine, but the applet's display of the
197 console will suffer artifacts.
200 <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>
201 <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.
206 subject = sorted(help_mapping.keys())
207 if not isinstance(subject, list):
210 return dict(simple=simple,
212 mapping=help_mapping)
213 help._cp_config['tools.require_login.on'] = False
215 def parseCreate(self, fields):
216 kws = dict([(kw, fields[kw]) for kw in
217 'name description owner memory disksize vmtype cdrom autoinstall'.split()
219 validate = validation.Validate(cherrypy.request.login,
220 cherrypy.request.state,
222 return dict(contact=cherrypy.request.login, name=validate.name,
223 description=validate.description, memory=validate.memory,
224 disksize=validate.disksize, owner=validate.owner,
225 machine_type=getattr(validate, 'vmtype', Defaults.type),
226 cdrom=getattr(validate, 'cdrom', None),
227 autoinstall=getattr(validate, 'autoinstall', None))
230 @cherrypy.tools.mako(filename="/list.mako")
231 @cherrypy.tools.require_POST()
232 def create(self, **fields):
233 """Handler for create requests."""
235 parsed_fields = self.parseCreate(fields)
236 machine = controls.createVm(cherrypy.request.login,
237 cherrypy.request.state, **parsed_fields)
238 except InvalidInput, err:
242 cherrypy.request.state.clear() #Changed global state
243 d = getListDict(cherrypy.request.login, cherrypy.request.state)
246 for field, value in fields.items():
247 setattr(d['defaults'], field, value)
249 d['new_machine'] = parsed_fields['name']
253 @cherrypy.tools.mako(filename="/helloworld.mako")
254 def helloworld(self, **kwargs):
255 return {'request': cherrypy.request, 'kwargs': kwargs}
256 helloworld._cp_config['tools.require_login.on'] = False
260 """Throw an error, to test the error-tracing mechanisms."""
261 print >>sys.stderr, "look ma, it's a stderr"
262 raise RuntimeError("test of the emergency broadcast system")
264 class MachineView(View):
265 def __getattr__(self, name):
266 """Synthesize attributes to allow RESTful URLs like
267 /machine/13/info. This is hairy. CherryPy 3.2 adds a
268 method called _cp_dispatch that allows you to explicitly
269 handle URLs that can't be mapped, and it allows you to
270 rewrite the path components and continue processing.
272 This function gets the next path component being resolved
273 as a string. _cp_dispatch will get an array of strings
274 representing any subsequent path components as well."""
277 cherrypy.request.params['machine_id'] = int(name)
283 @cherrypy.tools.mako(filename="/info.mako")
284 def info(self, machine_id):
285 """Handler for info on a single VM."""
286 machine = validation.Validate(cherrypy.request.login,
287 cherrypy.request.state,
288 machine_id=machine_id).machine
289 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
294 @cherrypy.tools.mako(filename="/info.mako")
295 @cherrypy.tools.require_POST()
296 def modify(self, machine_id, **fields):
297 """Handler for modifying attributes of a machine."""
299 modify_dict = modifyDict(cherrypy.request.login,
300 cherrypy.request.state,
302 except InvalidInput, err:
304 machine = validation.Validate(cherrypy.request.login,
305 cherrypy.request.state,
306 machine_id=machine_id).machine
308 machine = modify_dict['machine']
311 info_dict = infoDict(cherrypy.request.login,
312 cherrypy.request.state, machine)
313 info_dict['err'] = err
315 for field, value in fields.items():
316 setattr(info_dict['defaults'], field, value)
317 info_dict['result'] = result
321 @cherrypy.tools.mako(filename="/vnc.mako")
322 def vnc(self, machine_id):
325 Note that due to same-domain restrictions, the applet connects to
326 the webserver, which needs to forward those requests to the xen
327 server. The Xen server runs another proxy that (1) authenticates
328 and (2) finds the correct port for the VM.
330 You might want iptables like:
332 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
333 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
334 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
335 --dport 10003 -j SNAT --to-source 18.187.7.142
336 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
337 --dport 10003 -j ACCEPT
339 Remember to enable iptables!
340 echo 1 > /proc/sys/net/ipv4/ip_forward
342 machine = validation.Validate(cherrypy.request.login,
343 cherrypy.request.state,
344 machine_id=machine_id).machine
345 token = controls.vnctoken(machine)
346 host = controls.listHost(machine)
348 port = 10003 + [h.hostname for h in config.hosts].index(host)
352 status = controls.statusInfo(machine)
353 has_vnc = hasVnc(status)
358 hostname=cherrypy.request.local.name,
364 @cherrypy.tools.mako(filename="/command.mako")
365 @cherrypy.tools.require_POST()
366 def command(self, command_name, machine_id, **kwargs):
367 """Handler for running commands like boot and delete on a VM."""
368 back = kwargs.get('back')
369 if command_name == 'delete':
372 d = controls.commandResult(cherrypy.request.login,
373 cherrypy.request.state,
374 command_name, machine_id, kwargs)
375 except InvalidInput, err:
378 print >> sys.stderr, err
385 cherrypy.request.state.clear() #Changed global state
386 raise cherrypy.InternalRedirect('/list?result=%s'
387 % urllib.quote(result))
389 raise cherrypy.HTTPRedirect(cherrypy.request.base
390 + '/machine/%d/' % machine_id,
393 raise InvalidInput('back', back, 'Not a known back page.')
395 atmulti = ajaxterm.Multiplex()
397 atsessions_lock = threading.Lock()
400 @cherrypy.tools.mako(filename="/terminal.mako")
401 def terminal(self, machine_id):
402 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
404 status = controls.statusInfo(machine)
405 has_vnc = hasVnc(status)
410 hostname=cherrypy.request.local.name)
414 @cherrypy.tools.gzip()
415 def at(self, machine_id, k=None, c=0, force=0):
416 machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
417 with self.atsessions_lock:
418 if machine_id in self.atsessions:
419 term = self.atsessions[machine_id]
421 print >>sys.stderr, "spawning new session for terminal to ",machine_id
422 term = self.atsessions[machine_id] = self.atmulti.create(
423 ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
426 self.atmulti.proc_write(term,k)
428 dump=self.atmulti.dump(term,c,int(force))
429 cherrypy.response.headers['Content-Type']='text/xml'
430 if isinstance(dump,str):
433 print "Removing session for", machine_id
434 del self.atsessions[machine_id]
435 return '<?xml version="1.0"?><idem></idem>'
437 machine = MachineView()
441 """Class to store default values for fields."""
451 def __init__(self, max_memory=None, max_disk=None, **kws):
452 if max_memory is not None:
453 self.memory = min(self.memory, max_memory)
454 if max_disk is not None:
455 self.disk = min(self.disk, max_disk)
457 setattr(self, key, kws[key])
460 """Does the machine with a given status list support VNC?"""
464 if l[0] == 'device' and l[1][0] == 'vfb':
466 return 'location' in d
470 def getListDict(username, state):
471 """Gets the list of local variables used by list.tmpl."""
472 machines = state.machines
476 xmlist = state.xmlist
482 m.uptime = xmlist[m]['uptime']
483 installing[m] = bool(xmlist[m].get('autoinstall'))
484 if xmlist[m]['console']:
489 has_vnc[m] = "ParaVM"
490 max_memory = validation.maxMemory(username, state)
491 max_disk = validation.maxDisk(username)
492 defaults = Defaults(max_memory=max_memory,
495 def sortkey(machine):
496 return (machine.owner != username, machine.owner, machine.name)
497 machines = sorted(machines, key=sortkey)
498 d = dict(user=username,
499 cant_add_vm=validation.cantAddVm(username, state),
500 max_memory=max_memory,
505 installing=installing)
508 def getHostname(nic):
509 """Find the hostname associated with a NIC.
511 XXX this should be merged with the similar logic in DNS and DHCP.
514 hostname = nic.hostname
516 hostname = nic.machine.name
522 return hostname + '.' + config.dns.domains[0]
524 def getNicInfo(data_dict, machine):
525 """Helper function for info, get data on nics for a machine.
527 Modifies data_dict to include the relevant data, and returns a list
528 of (key, name) pairs to display "name: data_dict[key]" to the user.
530 data_dict['num_nics'] = len(machine.nics)
531 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
532 ('nic%s_mac', 'NIC %s MAC Addr'),
533 ('nic%s_ip', 'NIC %s IP'),
536 for i in range(len(machine.nics)):
537 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
538 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
539 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
540 data_dict['nic%s_ip' % i] = machine.nics[i].ip
541 if len(machine.nics) == 1:
542 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
545 def getDiskInfo(data_dict, machine):
546 """Helper function for info, get data on disks for a machine.
548 Modifies data_dict to include the relevant data, and returns a list
549 of (key, name) pairs to display "name: data_dict[key]" to the user.
551 data_dict['num_disks'] = len(machine.disks)
552 disk_fields_template = [('%s_size', '%s size')]
554 for disk in machine.disks:
555 name = disk.guest_device_name
556 disk_fields.extend([(x % name, y % name) for x, y in
557 disk_fields_template])
558 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
561 def modifyDict(username, state, machine_id, fields):
562 """Modify a machine as specified by CGI arguments.
564 Return a dict containing the machine that was modified.
569 kws = dict([(kw, fields[kw]) for kw in
570 'owner admin contact name description memory vmtype disksize'.split()
572 kws['machine_id'] = machine_id
573 validate = validation.Validate(username, state, **kws)
574 machine = validate.machine
575 oldname = machine.name
577 if hasattr(validate, 'memory'):
578 machine.memory = validate.memory
580 if hasattr(validate, 'vmtype'):
581 machine.type = validate.vmtype
583 if hasattr(validate, 'disksize'):
584 disksize = validate.disksize
585 disk = machine.disks[0]
586 if disk.size != disksize:
587 olddisk[disk.guest_device_name] = disksize
589 session.save_or_update(disk)
592 if hasattr(validate, 'owner') and validate.owner != machine.owner:
593 machine.owner = validate.owner
595 if hasattr(validate, 'name'):
596 machine.name = validate.name
597 for n in machine.nics:
598 if n.hostname == oldname:
599 n.hostname = validate.name
600 if hasattr(validate, 'description'):
601 machine.description = validate.description
602 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
603 machine.administrator = validate.admin
605 if hasattr(validate, 'contact'):
606 machine.contact = validate.contact
608 session.save_or_update(machine)
610 cache_acls.refreshMachine(machine)
615 for diskname in olddisk:
616 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
617 if hasattr(validate, 'name'):
618 controls.renameMachine(machine, oldname, validate.name)
619 return dict(machine=machine)
621 def infoDict(username, state, machine):
622 """Get the variables used by info.tmpl."""
623 status = controls.statusInfo(machine)
624 has_vnc = hasVnc(status)
626 main_status = dict(name=machine.name,
627 memory=str(machine.memory))
631 main_status = dict(status[1:])
632 main_status['host'] = controls.listHost(machine)
633 start_time = float(main_status.get('start_time', 0))
634 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
635 cpu_time_float = float(main_status.get('cpu_time', 0))
636 cputime = datetime.timedelta(seconds=int(cpu_time_float))
637 display_fields = [('name', 'Name'),
638 ('description', 'Description'),
640 ('administrator', 'Administrator'),
641 ('contact', 'Contact'),
644 ('uptime', 'uptime'),
645 ('cputime', 'CPU usage'),
646 ('host', 'Hosted on'),
649 ('state', 'state (xen format)'),
653 machine_info['name'] = machine.name
654 machine_info['description'] = machine.description
655 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
656 machine_info['owner'] = machine.owner
657 machine_info['administrator'] = machine.administrator
658 machine_info['contact'] = machine.contact
660 nic_fields = getNicInfo(machine_info, machine)
661 nic_point = display_fields.index('NIC_INFO')
662 display_fields = (display_fields[:nic_point] + nic_fields +
663 display_fields[nic_point+1:])
665 disk_fields = getDiskInfo(machine_info, machine)
666 disk_point = display_fields.index('DISK_INFO')
667 display_fields = (display_fields[:disk_point] + disk_fields +
668 display_fields[disk_point+1:])
670 main_status['memory'] += ' MiB'
671 for field, disp in display_fields:
672 if field in ('uptime', 'cputime') and locals()[field] is not None:
673 fields.append((disp, locals()[field]))
674 elif field in machine_info:
675 fields.append((disp, machine_info[field]))
676 elif field in main_status:
677 fields.append((disp, main_status[field]))
680 #fields.append((disp, None))
682 max_mem = validation.maxMemory(machine.owner, state, machine, False)
683 max_disk = validation.maxDisk(machine.owner, machine)
684 defaults = Defaults()
685 for name in 'machine_id name description administrator owner memory contact'.split():
686 if getattr(machine, name):
687 setattr(defaults, name, getattr(machine, name))
688 defaults.type = machine.type.type_id
689 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
690 d = dict(user=username,
691 on=status is not None,
702 def send_error_mail(subject, body):
705 to = config.web.errormail
711 """ % (to, config.web.hostname, subject, body)
712 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
713 stdin=subprocess.PIPE)