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)
409 machine = MachineView()
413 """Class to store default values for fields."""
423 def __init__(self, max_memory=None, max_disk=None, **kws):
424 if max_memory is not None:
425 self.memory = min(self.memory, max_memory)
426 if max_disk is not None:
427 self.disk = min(self.disk, max_disk)
429 setattr(self, key, kws[key])
432 """Does the machine with a given status list support VNC?"""
436 if l[0] == 'device' and l[1][0] == 'vfb':
438 return 'location' in d
442 def getListDict(username, state):
443 """Gets the list of local variables used by list.tmpl."""
444 machines = state.machines
448 xmlist = state.xmlist
454 m.uptime = xmlist[m]['uptime']
455 installing[m] = bool(xmlist[m].get('autoinstall'))
456 if xmlist[m]['console']:
461 has_vnc[m] = "ParaVM"
462 max_memory = validation.maxMemory(username, state)
463 max_disk = validation.maxDisk(username)
464 defaults = Defaults(max_memory=max_memory,
467 def sortkey(machine):
468 return (machine.owner != username, machine.owner, machine.name)
469 machines = sorted(machines, key=sortkey)
470 d = dict(user=username,
471 cant_add_vm=validation.cantAddVm(username, state),
472 max_memory=max_memory,
477 installing=installing)
480 def getHostname(nic):
481 """Find the hostname associated with a NIC.
483 XXX this should be merged with the similar logic in DNS and DHCP.
486 hostname = nic.hostname
488 hostname = nic.machine.name
494 return hostname + '.' + config.dns.domains[0]
496 def getNicInfo(data_dict, machine):
497 """Helper function for info, get data on nics for a machine.
499 Modifies data_dict to include the relevant data, and returns a list
500 of (key, name) pairs to display "name: data_dict[key]" to the user.
502 data_dict['num_nics'] = len(machine.nics)
503 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
504 ('nic%s_mac', 'NIC %s MAC Addr'),
505 ('nic%s_ip', 'NIC %s IP'),
508 for i in range(len(machine.nics)):
509 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
510 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
511 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
512 data_dict['nic%s_ip' % i] = machine.nics[i].ip
513 if len(machine.nics) == 1:
514 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
517 def getDiskInfo(data_dict, machine):
518 """Helper function for info, get data on disks 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_disks'] = len(machine.disks)
524 disk_fields_template = [('%s_size', '%s size')]
526 for disk in machine.disks:
527 name = disk.guest_device_name
528 disk_fields.extend([(x % name, y % name) for x, y in
529 disk_fields_template])
530 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
533 def modifyDict(username, state, machine_id, fields):
534 """Modify a machine as specified by CGI arguments.
536 Return a dict containing the machine that was modified.
541 kws = dict([(kw, fields[kw]) for kw in
542 'owner admin contact name description memory vmtype disksize'.split()
544 kws['machine_id'] = machine_id
545 validate = validation.Validate(username, state, **kws)
546 machine = validate.machine
547 oldname = machine.name
549 if hasattr(validate, 'memory'):
550 machine.memory = validate.memory
552 if hasattr(validate, 'vmtype'):
553 machine.type = validate.vmtype
555 if hasattr(validate, 'disksize'):
556 disksize = validate.disksize
557 disk = machine.disks[0]
558 if disk.size != disksize:
559 olddisk[disk.guest_device_name] = disksize
561 session.save_or_update(disk)
564 if hasattr(validate, 'owner') and validate.owner != machine.owner:
565 machine.owner = validate.owner
567 if hasattr(validate, 'name'):
568 machine.name = validate.name
569 for n in machine.nics:
570 if n.hostname == oldname:
571 n.hostname = validate.name
572 if hasattr(validate, 'description'):
573 machine.description = validate.description
574 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
575 machine.administrator = validate.admin
577 if hasattr(validate, 'contact'):
578 machine.contact = validate.contact
580 session.save_or_update(machine)
582 cache_acls.refreshMachine(machine)
587 for diskname in olddisk:
588 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
589 if hasattr(validate, 'name'):
590 controls.renameMachine(machine, oldname, validate.name)
591 return dict(machine=machine)
593 def infoDict(username, state, machine):
594 """Get the variables used by info.tmpl."""
595 status = controls.statusInfo(machine)
596 has_vnc = hasVnc(status)
598 main_status = dict(name=machine.name,
599 memory=str(machine.memory))
603 main_status = dict(status[1:])
604 main_status['host'] = controls.listHost(machine)
605 start_time = float(main_status.get('start_time', 0))
606 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
607 cpu_time_float = float(main_status.get('cpu_time', 0))
608 cputime = datetime.timedelta(seconds=int(cpu_time_float))
609 display_fields = [('name', 'Name'),
610 ('description', 'Description'),
612 ('administrator', 'Administrator'),
613 ('contact', 'Contact'),
616 ('uptime', 'uptime'),
617 ('cputime', 'CPU usage'),
618 ('host', 'Hosted on'),
621 ('state', 'state (xen format)'),
625 machine_info['name'] = machine.name
626 machine_info['description'] = machine.description
627 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
628 machine_info['owner'] = machine.owner
629 machine_info['administrator'] = machine.administrator
630 machine_info['contact'] = machine.contact
632 nic_fields = getNicInfo(machine_info, machine)
633 nic_point = display_fields.index('NIC_INFO')
634 display_fields = (display_fields[:nic_point] + nic_fields +
635 display_fields[nic_point+1:])
637 disk_fields = getDiskInfo(machine_info, machine)
638 disk_point = display_fields.index('DISK_INFO')
639 display_fields = (display_fields[:disk_point] + disk_fields +
640 display_fields[disk_point+1:])
642 main_status['memory'] += ' MiB'
643 for field, disp in display_fields:
644 if field in ('uptime', 'cputime') and locals()[field] is not None:
645 fields.append((disp, locals()[field]))
646 elif field in machine_info:
647 fields.append((disp, machine_info[field]))
648 elif field in main_status:
649 fields.append((disp, main_status[field]))
652 #fields.append((disp, None))
654 max_mem = validation.maxMemory(machine.owner, state, machine, False)
655 max_disk = validation.maxDisk(machine.owner, machine)
656 defaults = Defaults()
657 for name in 'machine_id name description administrator owner memory contact'.split():
658 if getattr(machine, name):
659 setattr(defaults, name, getattr(machine, name))
660 defaults.type = machine.type.type_id
661 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
662 d = dict(user=username,
663 on=status is not None,
674 def send_error_mail(subject, body):
677 to = config.web.errormail
683 """ % (to, config.web.hostname, subject, body)
684 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
685 stdin=subprocess.PIPE)