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
540 if hasattr(validate, 'disksize'):
541 disksize = validate.disksize
542 disk = machine.disks[0]
543 if disk.size != disksize:
544 olddisk[disk.guest_device_name] = disksize
546 session.save_or_update(disk)
549 if hasattr(validate, 'owner') and validate.owner != machine.owner:
550 machine.owner = validate.owner
552 if hasattr(validate, 'name'):
553 machine.name = validate.name
554 for n in machine.nics:
555 if n.hostname == oldname:
556 n.hostname = validate.name
557 if hasattr(validate, 'description'):
558 machine.description = validate.description
559 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
560 machine.administrator = validate.admin
562 if hasattr(validate, 'contact'):
563 machine.contact = validate.contact
565 session.save_or_update(machine)
567 cache_acls.refreshMachine(machine)
572 for diskname in olddisk:
573 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
574 if hasattr(validate, 'name'):
575 controls.renameMachine(machine, oldname, validate.name)
576 return dict(machine=machine)
578 def infoDict(username, state, machine):
579 """Get the variables used by info.tmpl."""
580 status = controls.statusInfo(machine)
581 has_vnc = hasVnc(status)
583 main_status = dict(name=machine.name,
584 memory=str(machine.memory))
588 main_status = dict(status[1:])
589 main_status['host'] = controls.listHost(machine)
590 start_time = float(main_status.get('start_time', 0))
591 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
592 cpu_time_float = float(main_status.get('cpu_time', 0))
593 cputime = datetime.timedelta(seconds=int(cpu_time_float))
594 display_fields = [('name', 'Name'),
595 ('description', 'Description'),
597 ('administrator', 'Administrator'),
598 ('contact', 'Contact'),
601 ('uptime', 'uptime'),
602 ('cputime', 'CPU usage'),
603 ('host', 'Hosted on'),
606 ('state', 'state (xen format)'),
610 machine_info['name'] = machine.name
611 machine_info['description'] = machine.description
612 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
613 machine_info['owner'] = machine.owner
614 machine_info['administrator'] = machine.administrator
615 machine_info['contact'] = machine.contact
617 nic_fields = getNicInfo(machine_info, machine)
618 nic_point = display_fields.index('NIC_INFO')
619 display_fields = (display_fields[:nic_point] + nic_fields +
620 display_fields[nic_point+1:])
622 disk_fields = getDiskInfo(machine_info, machine)
623 disk_point = display_fields.index('DISK_INFO')
624 display_fields = (display_fields[:disk_point] + disk_fields +
625 display_fields[disk_point+1:])
627 main_status['memory'] += ' MiB'
628 for field, disp in display_fields:
629 if field in ('uptime', 'cputime') and locals()[field] is not None:
630 fields.append((disp, locals()[field]))
631 elif field in machine_info:
632 fields.append((disp, machine_info[field]))
633 elif field in main_status:
634 fields.append((disp, main_status[field]))
637 #fields.append((disp, None))
639 max_mem = validation.maxMemory(machine.owner, state, machine, False)
640 max_disk = validation.maxDisk(machine.owner, machine)
641 defaults = Defaults()
642 for name in 'machine_id name description administrator owner memory contact'.split():
643 if getattr(machine, name):
644 setattr(defaults, name, getattr(machine, name))
645 defaults.type = machine.type.type_id
646 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
647 d = dict(user=username,
648 on=status is not None,
659 def send_error_mail(subject, body):
662 to = config.web.errormail
668 """ % (to, config.web.hostname, subject, body)
669 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
670 stdin=subprocess.PIPE)