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")
55 if os.path.exists("/etc/invirt/message"):
56 f = open('/etc/invirt/message')
57 d['serviceMessage']= f.read()
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 machine = MachineView()
399 """Class to store default values for fields."""
409 def __init__(self, max_memory=None, max_disk=None, **kws):
410 if max_memory is not None:
411 self.memory = min(self.memory, max_memory)
412 if max_disk is not None:
413 self.disk = min(self.disk, max_disk)
415 setattr(self, key, kws[key])
418 """Does the machine with a given status list support VNC?"""
422 if l[0] == 'device' and l[1][0] == 'vfb':
424 return 'location' in d
428 def getListDict(username, state):
429 """Gets the list of local variables used by list.tmpl."""
430 machines = state.machines
434 xmlist = state.xmlist
440 m.uptime = xmlist[m]['uptime']
441 installing[m] = bool(xmlist[m].get('autoinstall'))
442 if xmlist[m]['console']:
447 has_vnc[m] = "ParaVM"
448 max_memory = validation.maxMemory(username, state)
449 max_disk = validation.maxDisk(username)
450 defaults = Defaults(max_memory=max_memory,
453 def sortkey(machine):
454 return (machine.owner != username, machine.owner, machine.name)
455 machines = sorted(machines, key=sortkey)
456 d = dict(user=username,
457 cant_add_vm=validation.cantAddVm(username, state),
458 max_memory=max_memory,
463 installing=installing)
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
541 if hasattr(validate, 'disksize'):
542 disksize = validate.disksize
543 disk = machine.disks[0]
544 if disk.size != disksize:
545 olddisk[disk.guest_device_name] = disksize
547 session.save_or_update(disk)
550 if hasattr(validate, 'owner') and validate.owner != machine.owner:
551 machine.owner = validate.owner
553 if hasattr(validate, 'name'):
554 machine.name = validate.name
555 for n in machine.nics:
556 if n.hostname == oldname:
557 n.hostname = validate.name
558 if hasattr(validate, 'description'):
559 machine.description = validate.description
560 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
561 machine.administrator = validate.admin
563 if hasattr(validate, 'contact'):
564 machine.contact = validate.contact
566 session.save_or_update(machine)
568 cache_acls.refreshMachine(machine)
573 for diskname in olddisk:
574 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
575 if hasattr(validate, 'name'):
576 controls.renameMachine(machine, oldname, validate.name)
577 return dict(machine=machine)
579 def infoDict(username, state, machine):
580 """Get the variables used by info.tmpl."""
581 status = controls.statusInfo(machine)
582 has_vnc = hasVnc(status)
584 main_status = dict(name=machine.name,
585 memory=str(machine.memory))
589 main_status = dict(status[1:])
590 main_status['host'] = controls.listHost(machine)
591 start_time = float(main_status.get('start_time', 0))
592 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
593 cpu_time_float = float(main_status.get('cpu_time', 0))
594 cputime = datetime.timedelta(seconds=int(cpu_time_float))
595 display_fields = [('name', 'Name'),
596 ('description', 'Description'),
598 ('administrator', 'Administrator'),
599 ('contact', 'Contact'),
602 ('uptime', 'uptime'),
603 ('cputime', 'CPU usage'),
604 ('host', 'Hosted on'),
607 ('state', 'state (xen format)'),
611 machine_info['name'] = machine.name
612 machine_info['description'] = machine.description
613 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
614 machine_info['owner'] = machine.owner
615 machine_info['administrator'] = machine.administrator
616 machine_info['contact'] = machine.contact
618 nic_fields = getNicInfo(machine_info, machine)
619 nic_point = display_fields.index('NIC_INFO')
620 display_fields = (display_fields[:nic_point] + nic_fields +
621 display_fields[nic_point+1:])
623 disk_fields = getDiskInfo(machine_info, machine)
624 disk_point = display_fields.index('DISK_INFO')
625 display_fields = (display_fields[:disk_point] + disk_fields +
626 display_fields[disk_point+1:])
628 main_status['memory'] += ' MiB'
629 for field, disp in display_fields:
630 if field in ('uptime', 'cputime') and locals()[field] is not None:
631 fields.append((disp, locals()[field]))
632 elif field in machine_info:
633 fields.append((disp, machine_info[field]))
634 elif field in main_status:
635 fields.append((disp, main_status[field]))
638 #fields.append((disp, None))
640 max_mem = validation.maxMemory(machine.owner, state, machine, False)
641 max_disk = validation.maxDisk(machine.owner, machine)
642 defaults = Defaults()
643 for name in 'machine_id name description administrator owner memory contact'.split():
644 if getattr(machine, name):
645 setattr(defaults, name, getattr(machine, name))
646 defaults.type = machine.type.type_id
647 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
648 d = dict(user=username,
649 on=status is not None,
660 def send_error_mail(subject, body):
663 to = config.web.errormail
669 """ % (to, config.web.hostname, subject, body)
670 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
671 stdin=subprocess.PIPE)