2 """Main CGI script for web interface"""
16 from cherrypy import _cperror
17 from StringIO import StringIO
20 """Revert stderr to stdout, and print the contents of stderr"""
21 if isinstance(sys.stderr, StringIO):
22 print revertStandardError()
24 if __name__ == '__main__':
26 atexit.register(printError)
30 from webcommon import State
32 from getafsgroups import getAfsGroupMembers
33 from invirt import database
34 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
35 from invirt.config import structs as config
36 from invirt.common import InvalidInput, CodeError
38 from view import View, revertStandardError
40 class InvirtUnauthWeb(View):
42 @cherrypy.tools.mako(filename="/unauth.mako")
44 return {'simple': True}
46 class InvirtWeb(View):
48 super(self.__class__,self).__init__()
50 self._cp_config['tools.require_login.on'] = True
51 self._cp_config['tools.catch_stderr.on'] = True
52 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
53 'from invirt import database']
54 self._cp_config['request.error_response'] = self.handle_error
57 @cherrypy.tools.mako(filename="/invalid.mako")
58 def invalidInput(self):
59 """Print an error page when an InvalidInput exception occurs"""
60 err = cherrypy.request.prev.params["err"]
61 emsg = cherrypy.request.prev.params["emsg"]
62 d = dict(err_field=err.err_field,
63 err_value=str(err.err_value), stderr=emsg,
64 errorMessage=str(err))
68 @cherrypy.tools.mako(filename="/error.mako")
70 """Print an error page when an exception occurs"""
71 op = cherrypy.request.prev.path_info
72 username = cherrypy.request.login
73 err = cherrypy.request.prev.params["err"]
74 emsg = cherrypy.request.prev.params["emsg"]
75 traceback = cherrypy.request.prev.params["traceback"]
76 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
77 errorMessage=str(err), stderr=emsg, traceback=traceback)
78 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
79 details = error_raw.render(**d)
80 exclude = config.web.errormail_exclude
81 if username not in exclude and '*' not in exclude:
82 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
84 d['details'] = details
87 def __getattr__(self, name):
88 if name in ("admin", "overlord"):
89 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.cells[0].cell):
90 raise InvalidInput('username', cherrypy.request.login,
91 'Not in admin group %s.' % config.adminacl)
92 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
95 return super(InvirtWeb, self).__getattr__(name)
97 def handle_error(self):
98 err = sys.exc_info()[1]
99 if isinstance(err, InvalidInput):
100 cherrypy.request.params['err'] = err
101 cherrypy.request.params['emsg'] = revertStandardError()
102 raise cherrypy.InternalRedirect('/invalidInput')
103 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
104 cherrypy.request.params['err'] = err
105 cherrypy.request.params['emsg'] = revertStandardError()
106 cherrypy.request.params['traceback'] = _cperror.format_exc()
107 raise cherrypy.InternalRedirect('/error')
108 # fall back to cherrypy default error page
109 cherrypy.HTTPError(500).set_response()
112 @cherrypy.tools.mako(filename="/list.mako")
113 def list(self, result=None):
114 """Handler for list requests."""
115 checkpoint.checkpoint('Getting list dict')
116 d = getListDict(cherrypy.request.login, cherrypy.request.state)
117 if result is not None:
119 checkpoint.checkpoint('Got list dict')
124 @cherrypy.tools.mako(filename="/help.mako")
125 def help(self, subject=None, simple=False):
126 """Handler for help messages."""
130 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
131 ParaVM. You can access the resulting system by logging into the <a
132 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
133 with your Kerberos tickets; there is no root password so sshd will
136 <p>Under the covers, the autoinstaller uses our own patched version of
137 xen-create-image, which is a tool based on debootstrap. If you log
138 into the serial console while the install is running, you can watch
141 'ParaVM Console': """
142 ParaVM machines do not support local console access over VNC. To
143 access the serial console of these machines, you can SSH with Kerberos
144 to %s, using the name of the machine as your
145 username.""" % config.console.hostname,
147 HVM machines use the virtualization features of the processor, while
148 ParaVM machines rely on a modified kernel to communicate directly with
149 the hypervisor. HVMs support boot CDs of any operating system, and
150 the VNC console applet. The three-minute autoinstaller produces
151 ParaVMs. ParaVMs typically are more efficient, and always support the
152 <a href="help?subject=ParaVM+Console">console server</a>.</p>
154 <p>More details are <a
155 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
156 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
157 (which you can skip by using the autoinstaller to begin with.)</p>
159 <p>We recommend using a ParaVM when possible and an HVM when necessary.
162 Don't ask us! We're as mystified as you are.""",
164 The owner field is used to determine <a
165 href="help?subject=Quotas">quotas</a>. It must be the name of a
166 locker that you are an AFS administrator of. In particular, you or an
167 AFS group you are a member of must have AFS rlidwka bits on the
168 locker. You can check who administers the LOCKER locker using the
169 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
170 href="help?subject=Administrator">administrator</a>.""",
172 The administrator field determines who can access the console and
173 power on and off the machine. This can be either a user or a moira
176 Quotas are determined on a per-locker basis. Each locker may have a
177 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
180 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
181 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
182 your machine will run just fine, but the applet's display of the
183 console will suffer artifacts.
186 <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>
187 <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.
192 subject = sorted(help_mapping.keys())
193 if not isinstance(subject, list):
196 return dict(simple=simple,
198 mapping=help_mapping)
199 help._cp_config['tools.require_login.on'] = False
201 def parseCreate(self, fields):
202 kws = dict([(kw, fields[kw]) for kw in
203 'name description owner memory disksize vmtype cdrom autoinstall'.split()
205 validate = validation.Validate(cherrypy.request.login,
206 cherrypy.request.state,
208 return dict(contact=cherrypy.request.login, name=validate.name,
209 description=validate.description, memory=validate.memory,
210 disksize=validate.disksize, owner=validate.owner,
211 machine_type=getattr(validate, 'vmtype', Defaults.type),
212 cdrom=getattr(validate, 'cdrom', None),
213 autoinstall=getattr(validate, 'autoinstall', None))
216 @cherrypy.tools.mako(filename="/list.mako")
217 @cherrypy.tools.require_POST()
218 def create(self, **fields):
219 """Handler for create requests."""
221 parsed_fields = self.parseCreate(fields)
222 machine = controls.createVm(cherrypy.request.login,
223 cherrypy.request.state, **parsed_fields)
224 except InvalidInput, err:
228 cherrypy.request.state.clear() #Changed global state
229 d = getListDict(cherrypy.request.login, cherrypy.request.state)
232 for field, value in fields.items():
233 setattr(d['defaults'], field, value)
235 d['new_machine'] = parsed_fields['name']
239 @cherrypy.tools.mako(filename="/helloworld.mako")
240 def helloworld(self, **kwargs):
241 return {'request': cherrypy.request, 'kwargs': kwargs}
242 helloworld._cp_config['tools.require_login.on'] = False
246 """Throw an error, to test the error-tracing mechanisms."""
247 print >>sys.stderr, "look ma, it's a stderr"
248 raise RuntimeError("test of the emergency broadcast system")
250 class MachineView(View):
251 def __getattr__(self, name):
252 """Synthesize attributes to allow RESTful URLs like
253 /machine/13/info. This is hairy. CherryPy 3.2 adds a
254 method called _cp_dispatch that allows you to explicitly
255 handle URLs that can't be mapped, and it allows you to
256 rewrite the path components and continue processing.
258 This function gets the next path component being resolved
259 as a string. _cp_dispatch will get an array of strings
260 representing any subsequent path components as well."""
263 cherrypy.request.params['machine_id'] = int(name)
269 @cherrypy.tools.mako(filename="/info.mako")
270 def info(self, machine_id):
271 """Handler for info on a single VM."""
272 machine = validation.Validate(cherrypy.request.login,
273 cherrypy.request.state,
274 machine_id=machine_id).machine
275 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
276 checkpoint.checkpoint('Got infodict')
281 @cherrypy.tools.mako(filename="/info.mako")
282 @cherrypy.tools.require_POST()
283 def modify(self, machine_id, **fields):
284 """Handler for modifying attributes of a machine."""
286 modify_dict = modifyDict(cherrypy.request.login,
287 cherrypy.request.state,
289 except InvalidInput, err:
291 machine = validation.Validate(cherrypy.request.login,
292 cherrypy.request.state,
293 machine_id=machine_id).machine
295 machine = modify_dict['machine']
298 info_dict = infoDict(cherrypy.request.login,
299 cherrypy.request.state, machine)
300 info_dict['err'] = err
302 for field, value in fields.items():
303 setattr(info_dict['defaults'], field, value)
304 info_dict['result'] = result
308 @cherrypy.tools.mako(filename="/vnc.mako")
309 def vnc(self, machine_id):
312 Note that due to same-domain restrictions, the applet connects to
313 the webserver, which needs to forward those requests to the xen
314 server. The Xen server runs another proxy that (1) authenticates
315 and (2) finds the correct port for the VM.
317 You might want iptables like:
319 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
320 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
321 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
322 --dport 10003 -j SNAT --to-source 18.187.7.142
323 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
324 --dport 10003 -j ACCEPT
326 Remember to enable iptables!
327 echo 1 > /proc/sys/net/ipv4/ip_forward
329 machine = validation.Validate(cherrypy.request.login,
330 cherrypy.request.state,
331 machine_id=machine_id).machine
332 token = controls.vnctoken(machine)
333 host = controls.listHost(machine)
335 port = 10003 + [h.hostname for h in config.hosts].index(host)
339 status = controls.statusInfo(machine)
340 has_vnc = hasVnc(status)
345 hostname=cherrypy.request.local.name,
351 @cherrypy.tools.mako(filename="/command.mako")
352 @cherrypy.tools.require_POST()
353 def command(self, command_name, machine_id, **kwargs):
354 """Handler for running commands like boot and delete on a VM."""
355 back = kwargs.get('back')
357 d = controls.commandResult(cherrypy.request.login,
358 cherrypy.request.state,
359 command_name, machine_id, kwargs)
360 if d['command'] == 'Delete VM':
362 except InvalidInput, err:
365 print >> sys.stderr, err
372 cherrypy.request.state.clear() #Changed global state
373 raise cherrypy.InternalRedirect('/list?result=%s'
374 % urllib.quote(result))
376 raise cherrypy.HTTPRedirect(cherrypy.request.base
377 + '/machine/%d/' % machine_id,
380 raise InvalidInput('back', back, 'Not a known back page.')
382 machine = MachineView()
386 self.start_time = time.time()
387 self.checkpoints = []
389 def checkpoint(self, s):
390 self.checkpoints.append((s, time.time()))
393 return ('Timing info:\n%s\n' %
394 '\n'.join(['%s: %s' % (d, t - self.start_time) for
395 (d, t) in self.checkpoints]))
397 checkpoint = Checkpoint()
400 """Class to store default values for fields."""
410 def __init__(self, max_memory=None, max_disk=None, **kws):
411 if max_memory is not None:
412 self.memory = min(self.memory, max_memory)
413 if max_disk is not None:
414 self.disk = min(self.disk, max_disk)
416 setattr(self, key, kws[key])
419 """Does the machine with a given status list support VNC?"""
423 if l[0] == 'device' and l[1][0] == 'vfb':
425 return 'location' in d
429 def getListDict(username, state):
430 """Gets the list of local variables used by list.tmpl."""
431 checkpoint.checkpoint('Starting')
432 machines = state.machines
433 checkpoint.checkpoint('Got my machines')
437 xmlist = state.xmlist
438 checkpoint.checkpoint('Got uptimes')
444 m.uptime = xmlist[m]['uptime']
445 if xmlist[m]['console']:
450 has_vnc[m] = "ParaVM"
451 if xmlist[m].get('autoinstall'):
454 installing[m] = False
455 max_memory = validation.maxMemory(username, state)
456 max_disk = validation.maxDisk(username)
457 checkpoint.checkpoint('Got max mem/disk')
458 defaults = Defaults(max_memory=max_memory,
461 checkpoint.checkpoint('Got defaults')
462 def sortkey(machine):
463 return (machine.owner != username, machine.owner, machine.name)
464 machines = sorted(machines, key=sortkey)
465 d = dict(user=username,
466 cant_add_vm=validation.cantAddVm(username, state),
467 max_memory=max_memory,
472 installing=installing)
475 def getHostname(nic):
476 """Find the hostname associated with a NIC.
478 XXX this should be merged with the similar logic in DNS and DHCP.
481 hostname = nic.hostname
483 hostname = nic.machine.name
489 return hostname + '.' + config.dns.domains[0]
491 def getNicInfo(data_dict, machine):
492 """Helper function for info, get data on nics for a machine.
494 Modifies data_dict to include the relevant data, and returns a list
495 of (key, name) pairs to display "name: data_dict[key]" to the user.
497 data_dict['num_nics'] = len(machine.nics)
498 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
499 ('nic%s_mac', 'NIC %s MAC Addr'),
500 ('nic%s_ip', 'NIC %s IP'),
503 for i in range(len(machine.nics)):
504 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
505 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
506 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
507 data_dict['nic%s_ip' % i] = machine.nics[i].ip
508 if len(machine.nics) == 1:
509 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
512 def getDiskInfo(data_dict, machine):
513 """Helper function for info, get data on disks for a machine.
515 Modifies data_dict to include the relevant data, and returns a list
516 of (key, name) pairs to display "name: data_dict[key]" to the user.
518 data_dict['num_disks'] = len(machine.disks)
519 disk_fields_template = [('%s_size', '%s size')]
521 for disk in machine.disks:
522 name = disk.guest_device_name
523 disk_fields.extend([(x % name, y % name) for x, y in
524 disk_fields_template])
525 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
528 def modifyDict(username, state, machine_id, fields):
529 """Modify a machine as specified by CGI arguments.
531 Return a dict containing the machine that was modified.
536 kws = dict([(kw, fields[kw]) for kw in
537 'owner admin contact name description memory vmtype disksize'.split()
539 kws['machine_id'] = machine_id
540 validate = validation.Validate(username, state, **kws)
541 machine = validate.machine
542 oldname = machine.name
544 if hasattr(validate, 'memory'):
545 machine.memory = validate.memory
547 if hasattr(validate, 'vmtype'):
548 machine.type = validate.vmtype
550 if hasattr(validate, 'disksize'):
551 disksize = validate.disksize
552 disk = machine.disks[0]
553 if disk.size != disksize:
554 olddisk[disk.guest_device_name] = disksize
556 session.save_or_update(disk)
559 if hasattr(validate, 'owner') and validate.owner != machine.owner:
560 machine.owner = validate.owner
562 if hasattr(validate, 'name'):
563 machine.name = validate.name
564 for n in machine.nics:
565 if n.hostname == oldname:
566 n.hostname = validate.name
567 if hasattr(validate, 'description'):
568 machine.description = validate.description
569 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
570 machine.administrator = validate.admin
572 if hasattr(validate, 'contact'):
573 machine.contact = validate.contact
575 session.save_or_update(machine)
577 cache_acls.refreshMachine(machine)
582 for diskname in olddisk:
583 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
584 if hasattr(validate, 'name'):
585 controls.renameMachine(machine, oldname, validate.name)
586 return dict(machine=machine)
588 def infoDict(username, state, machine):
589 """Get the variables used by info.tmpl."""
590 status = controls.statusInfo(machine)
591 checkpoint.checkpoint('Getting status info')
592 has_vnc = hasVnc(status)
594 main_status = dict(name=machine.name,
595 memory=str(machine.memory))
599 main_status = dict(status[1:])
600 main_status['host'] = controls.listHost(machine)
601 start_time = float(main_status.get('start_time', 0))
602 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
603 cpu_time_float = float(main_status.get('cpu_time', 0))
604 cputime = datetime.timedelta(seconds=int(cpu_time_float))
605 checkpoint.checkpoint('Status')
606 display_fields = [('name', 'Name'),
607 ('description', 'Description'),
609 ('administrator', 'Administrator'),
610 ('contact', 'Contact'),
613 ('uptime', 'uptime'),
614 ('cputime', 'CPU usage'),
615 ('host', 'Hosted on'),
618 ('state', 'state (xen format)'),
622 machine_info['name'] = machine.name
623 machine_info['description'] = machine.description
624 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
625 machine_info['owner'] = machine.owner
626 machine_info['administrator'] = machine.administrator
627 machine_info['contact'] = machine.contact
629 nic_fields = getNicInfo(machine_info, machine)
630 nic_point = display_fields.index('NIC_INFO')
631 display_fields = (display_fields[:nic_point] + nic_fields +
632 display_fields[nic_point+1:])
634 disk_fields = getDiskInfo(machine_info, machine)
635 disk_point = display_fields.index('DISK_INFO')
636 display_fields = (display_fields[:disk_point] + disk_fields +
637 display_fields[disk_point+1:])
639 main_status['memory'] += ' MiB'
640 for field, disp in display_fields:
641 if field in ('uptime', 'cputime') and locals()[field] is not None:
642 fields.append((disp, locals()[field]))
643 elif field in machine_info:
644 fields.append((disp, machine_info[field]))
645 elif field in main_status:
646 fields.append((disp, main_status[field]))
649 #fields.append((disp, None))
651 checkpoint.checkpoint('Got fields')
654 max_mem = validation.maxMemory(machine.owner, state, machine, False)
655 checkpoint.checkpoint('Got mem')
656 max_disk = validation.maxDisk(machine.owner, machine)
657 defaults = Defaults()
658 for name in 'machine_id name description administrator owner memory contact'.split():
659 if getattr(machine, name):
660 setattr(defaults, name, getattr(machine, name))
661 defaults.type = machine.type.type_id
662 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
663 checkpoint.checkpoint('Got defaults')
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)