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)
465 def getHostname(nic):
466 """Find the hostname associated with a NIC.
468 XXX this should be merged with the similar logic in DNS and DHCP.
471 hostname = nic.hostname
473 hostname = nic.machine.name
479 return hostname + '.' + config.dns.domains[0]
481 def getNicInfo(data_dict, machine):
482 """Helper function for info, get data on nics for a machine.
484 Modifies data_dict to include the relevant data, and returns a list
485 of (key, name) pairs to display "name: data_dict[key]" to the user.
487 data_dict['num_nics'] = len(machine.nics)
488 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
489 ('nic%s_mac', 'NIC %s MAC Addr'),
490 ('nic%s_ip', 'NIC %s IP'),
493 for i in range(len(machine.nics)):
494 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
495 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
496 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
497 data_dict['nic%s_ip' % i] = machine.nics[i].ip
498 if len(machine.nics) == 1:
499 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
502 def getDiskInfo(data_dict, machine):
503 """Helper function for info, get data on disks for a machine.
505 Modifies data_dict to include the relevant data, and returns a list
506 of (key, name) pairs to display "name: data_dict[key]" to the user.
508 data_dict['num_disks'] = len(machine.disks)
509 disk_fields_template = [('%s_size', '%s size')]
511 for disk in machine.disks:
512 name = disk.guest_device_name
513 disk_fields.extend([(x % name, y % name) for x, y in
514 disk_fields_template])
515 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
518 def modifyDict(username, state, machine_id, fields):
519 """Modify a machine as specified by CGI arguments.
521 Return a dict containing the machine that was modified.
526 kws = dict((kw, fields[kw]) for kw in
527 'owner admin contact name description memory vmtype disksize'.split()
529 kws['machine_id'] = machine_id
530 validate = validation.Validate(username, state, **kws)
531 machine = validate.machine
532 oldname = machine.name
534 if hasattr(validate, 'memory'):
535 machine.memory = validate.memory
537 if hasattr(validate, 'vmtype'):
538 machine.type = validate.vmtype
541 if hasattr(validate, 'owner') and validate.owner != machine.owner:
542 machine.owner = validate.owner
544 if hasattr(validate, 'description'):
545 machine.description = validate.description
546 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
547 machine.administrator = validate.admin
549 if hasattr(validate, 'contact'):
550 machine.contact = validate.contact
552 session.save_or_update(machine)
560 if hasattr(validate, 'disksize'):
561 disksize = validate.disksize
562 disk = machine.disks[0]
563 if disk.size != disksize:
564 olddisk[disk.guest_device_name] = disksize
566 session.save_or_update(disk)
567 for diskname in olddisk:
568 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
569 session.save_or_update(machine)
577 if hasattr(validate, 'name'):
578 machine.name = validate.name
579 for n in machine.nics:
580 if n.hostname == oldname:
581 n.hostname = validate.name
582 if hasattr(validate, 'name'):
583 controls.renameMachine(machine, oldname, validate.name)
584 session.save_or_update(machine)
591 cache_acls.refreshMachine(machine)
593 return dict(machine=machine)
595 def infoDict(username, state, machine):
596 """Get the variables used by info.tmpl."""
597 status = controls.statusInfo(machine)
598 has_vnc = hasVnc(status)
600 main_status = dict(name=machine.name,
601 memory=str(machine.memory))
605 main_status = dict(status[1:])
606 main_status['host'] = controls.listHost(machine)
607 start_time = float(main_status.get('start_time', 0))
608 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
609 cpu_time_float = float(main_status.get('cpu_time', 0))
610 cputime = datetime.timedelta(seconds=int(cpu_time_float))
611 display_fields = [('name', 'Name'),
612 ('description', 'Description'),
614 ('administrator', 'Administrator'),
615 ('contact', 'Contact'),
618 ('uptime', 'uptime'),
619 ('cputime', 'CPU usage'),
620 ('host', 'Hosted on'),
623 ('state', 'state (xen format)'),
627 machine_info['name'] = machine.name
628 machine_info['description'] = machine.description
629 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
630 machine_info['owner'] = machine.owner
631 machine_info['administrator'] = machine.administrator
632 machine_info['contact'] = machine.contact
634 nic_fields = getNicInfo(machine_info, machine)
635 nic_point = display_fields.index('NIC_INFO')
636 display_fields = (display_fields[:nic_point] + nic_fields +
637 display_fields[nic_point+1:])
639 disk_fields = getDiskInfo(machine_info, machine)
640 disk_point = display_fields.index('DISK_INFO')
641 display_fields = (display_fields[:disk_point] + disk_fields +
642 display_fields[disk_point+1:])
644 main_status['memory'] += ' MiB'
645 for field, disp in display_fields:
646 if field in ('uptime', 'cputime') and locals()[field] is not None:
647 fields.append((disp, locals()[field]))
648 elif field in machine_info:
649 fields.append((disp, machine_info[field]))
650 elif field in main_status:
651 fields.append((disp, main_status[field]))
654 #fields.append((disp, None))
656 max_mem = validation.maxMemory(machine.owner, state, machine, False)
657 max_disk = validation.maxDisk(machine.owner, machine)
658 defaults = Defaults()
659 for name in 'machine_id name description administrator owner memory contact'.split():
660 if getattr(machine, name):
661 setattr(defaults, name, getattr(machine, name))
662 defaults.type = machine.type.type_id
663 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
664 d = dict(user=username,
665 on=status is not None,
676 def send_error_mail(subject, body):
679 to = config.web.errormail
685 """ % (to, config.web.hostname, subject, body)
686 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
687 stdin=subprocess.PIPE)