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
42 static_dir = os.path.join(os.path.dirname(__file__), 'static')
43 InvirtStatic = cherrypy.tools.staticdir.handler(
48 class InvirtUnauthWeb(View):
52 @cherrypy.tools.mako(filename="/unauth.mako")
54 return dict(simple=True)
56 class InvirtWeb(View):
58 super(self.__class__,self).__init__()
60 self._cp_config['tools.require_login.on'] = True
61 self._cp_config['tools.catch_stderr.on'] = True
62 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
63 'from invirt import database']
64 self._cp_config['request.error_response'] = self.handle_error
69 @cherrypy.tools.mako(filename="/invalid.mako")
70 def invalidInput(self):
71 """Print an error page when an InvalidInput exception occurs"""
72 err = cherrypy.request.prev.params["err"]
73 emsg = cherrypy.request.prev.params["emsg"]
74 d = dict(err_field=err.err_field,
75 err_value=str(err.err_value), stderr=emsg,
76 errorMessage=str(err))
80 @cherrypy.tools.mako(filename="/error.mako")
82 """Print an error page when an exception occurs"""
83 op = cherrypy.request.prev.path_info
84 username = cherrypy.request.login
85 err = cherrypy.request.prev.params["err"]
86 emsg = cherrypy.request.prev.params["emsg"]
87 traceback = cherrypy.request.prev.params["traceback"]
88 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
89 errorMessage=str(err), stderr=emsg, traceback=traceback)
90 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
91 details = error_raw.render(**d)
92 exclude = config.web.errormail_exclude
93 if username not in exclude and '*' not in exclude:
94 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
96 d['details'] = details
99 def __getattr__(self, name):
100 # At the point __getattr__ is called, tools haven't been run. Make sure the user is logged in.
101 cherrypy.tools.remote_user_login.callable()
103 if name in ("admin", "overlord"):
104 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
105 raise InvalidInput('username', cherrypy.request.login,
106 'Not in admin group %s.' % config.adminacl)
107 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
110 return super(InvirtWeb, self).__getattr__(name)
112 def handle_error(self):
113 err = sys.exc_info()[1]
114 if isinstance(err, InvalidInput):
115 cherrypy.request.params['err'] = err
116 cherrypy.request.params['emsg'] = revertStandardError()
117 raise cherrypy.InternalRedirect('/invalidInput')
118 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
119 cherrypy.request.params['err'] = err
120 cherrypy.request.params['emsg'] = revertStandardError()
121 cherrypy.request.params['traceback'] = _cperror.format_exc()
122 raise cherrypy.InternalRedirect('/error')
123 # fall back to cherrypy default error page
124 cherrypy.HTTPError(500).set_response()
127 @cherrypy.tools.mako(filename="/list.mako")
128 def list(self, result=None):
129 """Handler for list requests."""
130 d = getListDict(cherrypy.request.login, cherrypy.request.state)
131 if result is not None:
137 @cherrypy.tools.mako(filename="/help.mako")
138 def help(self, subject=None, simple=False):
139 """Handler for help messages."""
143 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
144 ParaVM. You can access the resulting system by logging into the <a
145 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
146 with your Kerberos tickets; there is no root password so sshd will
149 <p>Under the covers, the autoinstaller uses our own patched version of
150 xen-create-image, which is a tool based on debootstrap. If you log
151 into the serial console while the install is running, you can watch
154 'ParaVM Console': """
155 ParaVM machines do not support local console access over VNC. To
156 access the serial console of these machines, you can SSH with Kerberos
157 to %s, using the name of the machine as your
158 username.""" % config.console.hostname,
160 HVM machines use the virtualization features of the processor, while
161 ParaVM machines rely on a modified kernel to communicate directly with
162 the hypervisor. HVMs support boot CDs of any operating system, and
163 the VNC console applet. The three-minute autoinstaller produces
164 ParaVMs. ParaVMs typically are more efficient, and always support the
165 <a href="help?subject=ParaVM+Console">console server</a>.</p>
167 <p>More details are <a
168 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
169 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
170 (which you can skip by using the autoinstaller to begin with.)</p>
172 <p>We recommend using a ParaVM when possible and an HVM when necessary.
175 Don't ask us! We're as mystified as you are.""",
177 The owner field is used to determine <a
178 href="help?subject=Quotas">quotas</a>. It must be the name of a
179 locker that you are an AFS administrator of. In particular, you or an
180 AFS group you are a member of must have AFS rlidwka bits on the
181 locker. You can check who administers the LOCKER locker using the
182 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
183 href="help?subject=Administrator">administrator</a>.""",
185 The administrator field determines who can access the console and
186 power on and off the machine. This can be either a user or a moira
189 Quotas are determined on a per-locker basis. Each locker may have a
190 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
193 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
194 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
195 your machine will run just fine, but the applet's display of the
196 console will suffer artifacts.
199 <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>
200 <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.
205 subject = sorted(help_mapping.keys())
206 if not isinstance(subject, list):
209 return dict(simple=simple,
211 mapping=help_mapping)
212 help._cp_config['tools.require_login.on'] = False
214 def parseCreate(self, fields):
215 kws = dict([(kw, fields[kw]) for kw in
216 'name description owner memory disksize vmtype cdrom autoinstall'.split()
218 validate = validation.Validate(cherrypy.request.login,
219 cherrypy.request.state,
221 return dict(contact=cherrypy.request.login, name=validate.name,
222 description=validate.description, memory=validate.memory,
223 disksize=validate.disksize, owner=validate.owner,
224 machine_type=getattr(validate, 'vmtype', Defaults.type),
225 cdrom=getattr(validate, 'cdrom', None),
226 autoinstall=getattr(validate, 'autoinstall', None))
229 @cherrypy.tools.mako(filename="/list.mako")
230 @cherrypy.tools.require_POST()
231 def create(self, **fields):
232 """Handler for create requests."""
234 parsed_fields = self.parseCreate(fields)
235 machine = controls.createVm(cherrypy.request.login,
236 cherrypy.request.state, **parsed_fields)
237 except InvalidInput, err:
241 cherrypy.request.state.clear() #Changed global state
242 d = getListDict(cherrypy.request.login, cherrypy.request.state)
245 for field, value in fields.items():
246 setattr(d['defaults'], field, value)
248 d['new_machine'] = parsed_fields['name']
252 @cherrypy.tools.mako(filename="/helloworld.mako")
253 def helloworld(self, **kwargs):
254 return {'request': cherrypy.request, 'kwargs': kwargs}
255 helloworld._cp_config['tools.require_login.on'] = False
259 """Throw an error, to test the error-tracing mechanisms."""
260 print >>sys.stderr, "look ma, it's a stderr"
261 raise RuntimeError("test of the emergency broadcast system")
263 class MachineView(View):
264 def __getattr__(self, name):
265 """Synthesize attributes to allow RESTful URLs like
266 /machine/13/info. This is hairy. CherryPy 3.2 adds a
267 method called _cp_dispatch that allows you to explicitly
268 handle URLs that can't be mapped, and it allows you to
269 rewrite the path components and continue processing.
271 This function gets the next path component being resolved
272 as a string. _cp_dispatch will get an array of strings
273 representing any subsequent path components as well."""
276 cherrypy.request.params['machine_id'] = int(name)
282 @cherrypy.tools.mako(filename="/info.mako")
283 def info(self, machine_id):
284 """Handler for info on a single VM."""
285 machine = validation.Validate(cherrypy.request.login,
286 cherrypy.request.state,
287 machine_id=machine_id).machine
288 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
293 @cherrypy.tools.mako(filename="/info.mako")
294 @cherrypy.tools.require_POST()
295 def modify(self, machine_id, **fields):
296 """Handler for modifying attributes of a machine."""
298 modify_dict = modifyDict(cherrypy.request.login,
299 cherrypy.request.state,
301 except InvalidInput, err:
303 machine = validation.Validate(cherrypy.request.login,
304 cherrypy.request.state,
305 machine_id=machine_id).machine
307 machine = modify_dict['machine']
310 info_dict = infoDict(cherrypy.request.login,
311 cherrypy.request.state, machine)
312 info_dict['err'] = err
314 for field, value in fields.items():
315 setattr(info_dict['defaults'], field, value)
316 info_dict['result'] = result
320 @cherrypy.tools.mako(filename="/vnc.mako")
321 def vnc(self, machine_id):
324 Note that due to same-domain restrictions, the applet connects to
325 the webserver, which needs to forward those requests to the xen
326 server. The Xen server runs another proxy that (1) authenticates
327 and (2) finds the correct port for the VM.
329 You might want iptables like:
331 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
332 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
333 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
334 --dport 10003 -j SNAT --to-source 18.187.7.142
335 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
336 --dport 10003 -j ACCEPT
338 Remember to enable iptables!
339 echo 1 > /proc/sys/net/ipv4/ip_forward
341 machine = validation.Validate(cherrypy.request.login,
342 cherrypy.request.state,
343 machine_id=machine_id).machine
344 token = controls.vnctoken(machine)
345 host = controls.listHost(machine)
347 port = 10003 + [h.hostname for h in config.hosts].index(host)
351 status = controls.statusInfo(machine)
352 has_vnc = hasVnc(status)
357 hostname=cherrypy.request.local.name,
363 @cherrypy.tools.mako(filename="/command.mako")
364 @cherrypy.tools.require_POST()
365 def command(self, command_name, machine_id, **kwargs):
366 """Handler for running commands like boot and delete on a VM."""
367 back = kwargs.get('back')
368 if command_name == 'delete':
371 d = controls.commandResult(cherrypy.request.login,
372 cherrypy.request.state,
373 command_name, machine_id, kwargs)
374 except InvalidInput, err:
377 print >> sys.stderr, err
384 cherrypy.request.state.clear() #Changed global state
385 raise cherrypy.InternalRedirect('/list?result=%s'
386 % urllib.quote(result))
388 raise cherrypy.HTTPRedirect(cherrypy.request.base
389 + '/machine/%d/' % machine_id,
392 raise InvalidInput('back', back, 'Not a known back page.')
394 machine = MachineView()
398 """Class to store default values for fields."""
408 def __init__(self, max_memory=None, max_disk=None, **kws):
409 if max_memory is not None:
410 self.memory = min(self.memory, max_memory)
411 if max_disk is not None:
412 self.disk = min(self.disk, max_disk)
414 setattr(self, key, kws[key])
417 """Does the machine with a given status list support VNC?"""
421 if l[0] == 'device' and l[1][0] == 'vfb':
423 return 'location' in d
427 def getListDict(username, state):
428 """Gets the list of local variables used by list.tmpl."""
429 machines = state.machines
433 xmlist = state.xmlist
439 m.uptime = xmlist[m]['uptime']
440 installing[m] = bool(xmlist[m].get('autoinstall'))
441 if xmlist[m]['console']:
446 has_vnc[m] = "ParaVM"
447 max_memory = validation.maxMemory(username, state)
448 max_disk = validation.maxDisk(username)
449 defaults = Defaults(max_memory=max_memory,
452 def sortkey(machine):
453 return (machine.owner != username, machine.owner, machine.name)
454 machines = sorted(machines, key=sortkey)
455 d = dict(user=username,
456 cant_add_vm=validation.cantAddVm(username, state),
457 max_memory=max_memory,
462 installing=installing,
463 disable_creation=False)
466 def getHostname(nic):
467 """Find the hostname associated with a NIC.
469 XXX this should be merged with the similar logic in DNS and DHCP.
472 hostname = nic.hostname
474 hostname = nic.machine.name
480 return hostname + '.' + config.dns.domains[0]
482 def getNicInfo(data_dict, machine):
483 """Helper function for info, get data on nics for a machine.
485 Modifies data_dict to include the relevant data, and returns a list
486 of (key, name) pairs to display "name: data_dict[key]" to the user.
488 data_dict['num_nics'] = len(machine.nics)
489 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
490 ('nic%s_mac', 'NIC %s MAC Addr'),
491 ('nic%s_ip', 'NIC %s IP'),
494 for i in range(len(machine.nics)):
495 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
496 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
497 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
498 data_dict['nic%s_ip' % i] = machine.nics[i].ip
499 if len(machine.nics) == 1:
500 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
503 def getDiskInfo(data_dict, machine):
504 """Helper function for info, get data on disks for a machine.
506 Modifies data_dict to include the relevant data, and returns a list
507 of (key, name) pairs to display "name: data_dict[key]" to the user.
509 data_dict['num_disks'] = len(machine.disks)
510 disk_fields_template = [('%s_size', '%s size')]
512 for disk in machine.disks:
513 name = disk.guest_device_name
514 disk_fields.extend([(x % name, y % name) for x, y in
515 disk_fields_template])
516 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
519 def modifyDict(username, state, machine_id, fields):
520 """Modify a machine as specified by CGI arguments.
522 Return a dict containing the machine that was modified.
527 kws = dict((kw, fields[kw]) for kw in
528 'owner admin contact name description memory vmtype disksize'.split()
530 kws['machine_id'] = machine_id
531 validate = validation.Validate(username, state, **kws)
532 machine = validate.machine
533 oldname = machine.name
535 if hasattr(validate, 'memory'):
536 machine.memory = validate.memory
538 if hasattr(validate, 'vmtype'):
539 machine.type = validate.vmtype
542 if hasattr(validate, 'owner') and validate.owner != machine.owner:
543 machine.owner = validate.owner
545 if hasattr(validate, 'description'):
546 machine.description = validate.description
547 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
548 machine.administrator = validate.admin
550 if hasattr(validate, 'contact'):
551 machine.contact = validate.contact
553 session.save_or_update(machine)
561 if hasattr(validate, 'disksize'):
562 disksize = validate.disksize
563 disk = machine.disks[0]
564 if disk.size != disksize:
565 olddisk[disk.guest_device_name] = disksize
567 session.save_or_update(disk)
568 for diskname in olddisk:
569 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
570 session.save_or_update(machine)
578 if hasattr(validate, 'name'):
579 machine.name = validate.name
580 for n in machine.nics:
581 if n.hostname == oldname:
582 n.hostname = validate.name
583 if hasattr(validate, 'name'):
584 controls.renameMachine(machine, oldname, validate.name)
585 session.save_or_update(machine)
592 cache_acls.refreshMachine(machine)
594 return dict(machine=machine)
596 def infoDict(username, state, machine):
597 """Get the variables used by info.tmpl."""
598 status = controls.statusInfo(machine)
599 has_vnc = hasVnc(status)
601 main_status = dict(name=machine.name,
602 memory=str(machine.memory))
606 main_status = dict(status[1:])
607 main_status['host'] = controls.listHost(machine)
608 start_time = main_status.get('start_time')
609 if start_time is None:
610 uptime = "Still booting?"
612 start_time = float(start_time)
613 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
614 cpu_time_float = float(main_status.get('cpu_time', 0))
615 cputime = datetime.timedelta(seconds=int(cpu_time_float))
616 display_fields = [('name', 'Name'),
617 ('description', 'Description'),
619 ('administrator', 'Administrator'),
620 ('contact', 'Contact'),
623 ('uptime', 'uptime'),
624 ('cputime', 'CPU usage'),
625 ('host', 'Hosted on'),
628 ('state', 'state (xen format)'),
632 machine_info['name'] = machine.name
633 machine_info['description'] = machine.description
634 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
635 machine_info['owner'] = machine.owner
636 machine_info['administrator'] = machine.administrator
637 machine_info['contact'] = machine.contact
639 nic_fields = getNicInfo(machine_info, machine)
640 nic_point = display_fields.index('NIC_INFO')
641 display_fields = (display_fields[:nic_point] + nic_fields +
642 display_fields[nic_point+1:])
644 disk_fields = getDiskInfo(machine_info, machine)
645 disk_point = display_fields.index('DISK_INFO')
646 display_fields = (display_fields[:disk_point] + disk_fields +
647 display_fields[disk_point+1:])
649 main_status['memory'] += ' MiB'
650 for field, disp in display_fields:
651 if field in ('uptime', 'cputime') and locals()[field] is not None:
652 fields.append((disp, locals()[field]))
653 elif field in machine_info:
654 fields.append((disp, machine_info[field]))
655 elif field in main_status:
656 fields.append((disp, main_status[field]))
659 #fields.append((disp, None))
661 max_mem = validation.maxMemory(machine.owner, state, machine, False)
662 max_disk = validation.maxDisk(machine.owner, machine)
663 defaults = Defaults()
664 for name in 'machine_id name description administrator owner memory contact'.split():
665 if getattr(machine, name):
666 setattr(defaults, name, getattr(machine, name))
667 defaults.type = machine.type.type_id
668 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
669 d = dict(user=username,
670 on=status is not None,
681 def send_error_mail(subject, body):
684 to = config.web.errormail
690 """ % (to, config.web.hostname, subject, body)
691 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
692 stdin=subprocess.PIPE)