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 if(os.path.exists("/etc/invirt/message")):
55 f = open('/etc/invirt/message')
58 d = dict(simple = True, serviceMessage = message)
62 class InvirtWeb(View):
64 super(self.__class__,self).__init__()
66 self._cp_config['tools.require_login.on'] = True
67 self._cp_config['tools.catch_stderr.on'] = True
68 self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
69 'from invirt import database']
70 self._cp_config['request.error_response'] = self.handle_error
75 @cherrypy.tools.mako(filename="/invalid.mako")
76 def invalidInput(self):
77 """Print an error page when an InvalidInput exception occurs"""
78 err = cherrypy.request.prev.params["err"]
79 emsg = cherrypy.request.prev.params["emsg"]
80 d = dict(err_field=err.err_field,
81 err_value=str(err.err_value), stderr=emsg,
82 errorMessage=str(err))
86 @cherrypy.tools.mako(filename="/error.mako")
88 """Print an error page when an exception occurs"""
89 op = cherrypy.request.prev.path_info
90 username = cherrypy.request.login
91 err = cherrypy.request.prev.params["err"]
92 emsg = cherrypy.request.prev.params["emsg"]
93 traceback = cherrypy.request.prev.params["traceback"]
94 d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
95 errorMessage=str(err), stderr=emsg, traceback=traceback)
96 error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
97 details = error_raw.render(**d)
98 exclude = config.web.errormail_exclude
99 if username not in exclude and '*' not in exclude:
100 send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
102 d['details'] = details
105 def __getattr__(self, name):
106 if name in ("admin", "overlord"):
107 if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
108 raise InvalidInput('username', cherrypy.request.login,
109 'Not in admin group %s.' % config.adminacl)
110 cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
113 return super(InvirtWeb, self).__getattr__(name)
115 def handle_error(self):
116 err = sys.exc_info()[1]
117 if isinstance(err, InvalidInput):
118 cherrypy.request.params['err'] = err
119 cherrypy.request.params['emsg'] = revertStandardError()
120 raise cherrypy.InternalRedirect('/invalidInput')
121 if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
122 cherrypy.request.params['err'] = err
123 cherrypy.request.params['emsg'] = revertStandardError()
124 cherrypy.request.params['traceback'] = _cperror.format_exc()
125 raise cherrypy.InternalRedirect('/error')
126 # fall back to cherrypy default error page
127 cherrypy.HTTPError(500).set_response()
130 @cherrypy.tools.mako(filename="/list.mako")
131 def list(self, result=None):
132 """Handler for list requests."""
133 d = getListDict(cherrypy.request.login, cherrypy.request.state)
134 if result is not None:
140 @cherrypy.tools.mako(filename="/help.mako")
141 def help(self, subject=None, simple=False):
142 """Handler for help messages."""
146 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
147 ParaVM. You can access the resulting system by logging into the <a
148 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
149 with your Kerberos tickets; there is no root password so sshd will
152 <p>Under the covers, the autoinstaller uses our own patched version of
153 xen-create-image, which is a tool based on debootstrap. If you log
154 into the serial console while the install is running, you can watch
157 'ParaVM Console': """
158 ParaVM machines do not support local console access over VNC. To
159 access the serial console of these machines, you can SSH with Kerberos
160 to %s, using the name of the machine as your
161 username.""" % config.console.hostname,
163 HVM machines use the virtualization features of the processor, while
164 ParaVM machines rely on a modified kernel to communicate directly with
165 the hypervisor. HVMs support boot CDs of any operating system, and
166 the VNC console applet. The three-minute autoinstaller produces
167 ParaVMs. ParaVMs typically are more efficient, and always support the
168 <a href="help?subject=ParaVM+Console">console server</a>.</p>
170 <p>More details are <a
171 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
172 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
173 (which you can skip by using the autoinstaller to begin with.)</p>
175 <p>We recommend using a ParaVM when possible and an HVM when necessary.
178 Don't ask us! We're as mystified as you are.""",
180 The owner field is used to determine <a
181 href="help?subject=Quotas">quotas</a>. It must be the name of a
182 locker that you are an AFS administrator of. In particular, you or an
183 AFS group you are a member of must have AFS rlidwka bits on the
184 locker. You can check who administers the LOCKER locker using the
185 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
186 href="help?subject=Administrator">administrator</a>.""",
188 The administrator field determines who can access the console and
189 power on and off the machine. This can be either a user or a moira
192 Quotas are determined on a per-locker basis. Each locker may have a
193 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
196 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
197 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
198 your machine will run just fine, but the applet's display of the
199 console will suffer artifacts.
202 <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>
203 <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.
208 subject = sorted(help_mapping.keys())
209 if not isinstance(subject, list):
212 return dict(simple=simple,
214 mapping=help_mapping)
215 help._cp_config['tools.require_login.on'] = False
217 def parseCreate(self, fields):
218 kws = dict([(kw, fields[kw]) for kw in
219 'name description owner memory disksize vmtype cdrom autoinstall'.split()
221 validate = validation.Validate(cherrypy.request.login,
222 cherrypy.request.state,
224 return dict(contact=cherrypy.request.login, name=validate.name,
225 description=validate.description, memory=validate.memory,
226 disksize=validate.disksize, owner=validate.owner,
227 machine_type=getattr(validate, 'vmtype', Defaults.type),
228 cdrom=getattr(validate, 'cdrom', None),
229 autoinstall=getattr(validate, 'autoinstall', None))
232 @cherrypy.tools.mako(filename="/list.mako")
233 @cherrypy.tools.require_POST()
234 def create(self, **fields):
235 """Handler for create requests."""
237 parsed_fields = self.parseCreate(fields)
238 machine = controls.createVm(cherrypy.request.login,
239 cherrypy.request.state, **parsed_fields)
240 except InvalidInput, err:
244 cherrypy.request.state.clear() #Changed global state
245 d = getListDict(cherrypy.request.login, cherrypy.request.state)
248 for field, value in fields.items():
249 setattr(d['defaults'], field, value)
251 d['new_machine'] = parsed_fields['name']
255 @cherrypy.tools.mako(filename="/helloworld.mako")
256 def helloworld(self, **kwargs):
257 return {'request': cherrypy.request, 'kwargs': kwargs}
258 helloworld._cp_config['tools.require_login.on'] = False
262 """Throw an error, to test the error-tracing mechanisms."""
263 print >>sys.stderr, "look ma, it's a stderr"
264 raise RuntimeError("test of the emergency broadcast system")
266 class MachineView(View):
267 def __getattr__(self, name):
268 """Synthesize attributes to allow RESTful URLs like
269 /machine/13/info. This is hairy. CherryPy 3.2 adds a
270 method called _cp_dispatch that allows you to explicitly
271 handle URLs that can't be mapped, and it allows you to
272 rewrite the path components and continue processing.
274 This function gets the next path component being resolved
275 as a string. _cp_dispatch will get an array of strings
276 representing any subsequent path components as well."""
279 cherrypy.request.params['machine_id'] = int(name)
285 @cherrypy.tools.mako(filename="/info.mako")
286 def info(self, machine_id):
287 """Handler for info on a single VM."""
288 machine = validation.Validate(cherrypy.request.login,
289 cherrypy.request.state,
290 machine_id=machine_id).machine
291 d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
296 @cherrypy.tools.mako(filename="/info.mako")
297 @cherrypy.tools.require_POST()
298 def modify(self, machine_id, **fields):
299 """Handler for modifying attributes of a machine."""
301 modify_dict = modifyDict(cherrypy.request.login,
302 cherrypy.request.state,
304 except InvalidInput, err:
306 machine = validation.Validate(cherrypy.request.login,
307 cherrypy.request.state,
308 machine_id=machine_id).machine
310 machine = modify_dict['machine']
313 info_dict = infoDict(cherrypy.request.login,
314 cherrypy.request.state, machine)
315 info_dict['err'] = err
317 for field, value in fields.items():
318 setattr(info_dict['defaults'], field, value)
319 info_dict['result'] = result
323 @cherrypy.tools.mako(filename="/vnc.mako")
324 def vnc(self, machine_id):
327 Note that due to same-domain restrictions, the applet connects to
328 the webserver, which needs to forward those requests to the xen
329 server. The Xen server runs another proxy that (1) authenticates
330 and (2) finds the correct port for the VM.
332 You might want iptables like:
334 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
335 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
336 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
337 --dport 10003 -j SNAT --to-source 18.187.7.142
338 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
339 --dport 10003 -j ACCEPT
341 Remember to enable iptables!
342 echo 1 > /proc/sys/net/ipv4/ip_forward
344 machine = validation.Validate(cherrypy.request.login,
345 cherrypy.request.state,
346 machine_id=machine_id).machine
347 token = controls.vnctoken(machine)
348 host = controls.listHost(machine)
350 port = 10003 + [h.hostname for h in config.hosts].index(host)
354 status = controls.statusInfo(machine)
355 has_vnc = hasVnc(status)
360 hostname=cherrypy.request.local.name,
366 @cherrypy.tools.mako(filename="/command.mako")
367 @cherrypy.tools.require_POST()
368 def command(self, command_name, machine_id, **kwargs):
369 """Handler for running commands like boot and delete on a VM."""
370 back = kwargs.get('back')
371 if command_name == 'delete':
374 d = controls.commandResult(cherrypy.request.login,
375 cherrypy.request.state,
376 command_name, machine_id, kwargs)
377 except InvalidInput, err:
380 print >> sys.stderr, err
387 cherrypy.request.state.clear() #Changed global state
388 raise cherrypy.InternalRedirect('/list?result=%s'
389 % urllib.quote(result))
391 raise cherrypy.HTTPRedirect(cherrypy.request.base
392 + '/machine/%d/' % machine_id,
395 raise InvalidInput('back', back, 'Not a known back page.')
397 machine = MachineView()
401 """Class to store default values for fields."""
411 def __init__(self, max_memory=None, max_disk=None, **kws):
412 if max_memory is not None:
413 self.memory = min(self.memory, max_memory)
414 if max_disk is not None:
415 self.disk = min(self.disk, max_disk)
417 setattr(self, key, kws[key])
420 """Does the machine with a given status list support VNC?"""
424 if l[0] == 'device' and l[1][0] == 'vfb':
426 return 'location' in d
430 def getListDict(username, state):
431 """Gets the list of local variables used by list.tmpl."""
432 machines = state.machines
436 xmlist = state.xmlist
442 m.uptime = xmlist[m]['uptime']
443 installing[m] = bool(xmlist[m].get('autoinstall'))
444 if xmlist[m]['console']:
449 has_vnc[m] = "ParaVM"
450 max_memory = validation.maxMemory(username, state)
451 max_disk = validation.maxDisk(username)
452 defaults = Defaults(max_memory=max_memory,
455 def sortkey(machine):
456 return (machine.owner != username, machine.owner, machine.name)
457 machines = sorted(machines, key=sortkey)
458 d = dict(user=username,
459 cant_add_vm=validation.cantAddVm(username, state),
460 max_memory=max_memory,
465 installing=installing)
468 def getHostname(nic):
469 """Find the hostname associated with a NIC.
471 XXX this should be merged with the similar logic in DNS and DHCP.
474 hostname = nic.hostname
476 hostname = nic.machine.name
482 return hostname + '.' + config.dns.domains[0]
484 def getNicInfo(data_dict, machine):
485 """Helper function for info, get data on nics for a machine.
487 Modifies data_dict to include the relevant data, and returns a list
488 of (key, name) pairs to display "name: data_dict[key]" to the user.
490 data_dict['num_nics'] = len(machine.nics)
491 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
492 ('nic%s_mac', 'NIC %s MAC Addr'),
493 ('nic%s_ip', 'NIC %s IP'),
496 for i in range(len(machine.nics)):
497 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
498 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
499 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
500 data_dict['nic%s_ip' % i] = machine.nics[i].ip
501 if len(machine.nics) == 1:
502 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
505 def getDiskInfo(data_dict, machine):
506 """Helper function for info, get data on disks for a machine.
508 Modifies data_dict to include the relevant data, and returns a list
509 of (key, name) pairs to display "name: data_dict[key]" to the user.
511 data_dict['num_disks'] = len(machine.disks)
512 disk_fields_template = [('%s_size', '%s size')]
514 for disk in machine.disks:
515 name = disk.guest_device_name
516 disk_fields.extend([(x % name, y % name) for x, y in
517 disk_fields_template])
518 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
521 def modifyDict(username, state, machine_id, fields):
522 """Modify a machine as specified by CGI arguments.
524 Return a dict containing the machine that was modified.
529 kws = dict([(kw, fields[kw]) for kw in
530 'owner admin contact name description memory vmtype disksize'.split()
532 kws['machine_id'] = machine_id
533 validate = validation.Validate(username, state, **kws)
534 machine = validate.machine
535 oldname = machine.name
537 if hasattr(validate, 'memory'):
538 machine.memory = validate.memory
540 if hasattr(validate, 'vmtype'):
541 machine.type = validate.vmtype
543 if hasattr(validate, 'disksize'):
544 disksize = validate.disksize
545 disk = machine.disks[0]
546 if disk.size != disksize:
547 olddisk[disk.guest_device_name] = disksize
549 session.save_or_update(disk)
552 if hasattr(validate, 'owner') and validate.owner != machine.owner:
553 machine.owner = validate.owner
555 if hasattr(validate, 'name'):
556 machine.name = validate.name
557 for n in machine.nics:
558 if n.hostname == oldname:
559 n.hostname = validate.name
560 if hasattr(validate, 'description'):
561 machine.description = validate.description
562 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
563 machine.administrator = validate.admin
565 if hasattr(validate, 'contact'):
566 machine.contact = validate.contact
568 session.save_or_update(machine)
570 cache_acls.refreshMachine(machine)
575 for diskname in olddisk:
576 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
577 if hasattr(validate, 'name'):
578 controls.renameMachine(machine, oldname, validate.name)
579 return dict(machine=machine)
581 def infoDict(username, state, machine):
582 """Get the variables used by info.tmpl."""
583 status = controls.statusInfo(machine)
584 has_vnc = hasVnc(status)
586 main_status = dict(name=machine.name,
587 memory=str(machine.memory))
591 main_status = dict(status[1:])
592 main_status['host'] = controls.listHost(machine)
593 start_time = float(main_status.get('start_time', 0))
594 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
595 cpu_time_float = float(main_status.get('cpu_time', 0))
596 cputime = datetime.timedelta(seconds=int(cpu_time_float))
597 display_fields = [('name', 'Name'),
598 ('description', 'Description'),
600 ('administrator', 'Administrator'),
601 ('contact', 'Contact'),
604 ('uptime', 'uptime'),
605 ('cputime', 'CPU usage'),
606 ('host', 'Hosted on'),
609 ('state', 'state (xen format)'),
613 machine_info['name'] = machine.name
614 machine_info['description'] = machine.description
615 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
616 machine_info['owner'] = machine.owner
617 machine_info['administrator'] = machine.administrator
618 machine_info['contact'] = machine.contact
620 nic_fields = getNicInfo(machine_info, machine)
621 nic_point = display_fields.index('NIC_INFO')
622 display_fields = (display_fields[:nic_point] + nic_fields +
623 display_fields[nic_point+1:])
625 disk_fields = getDiskInfo(machine_info, machine)
626 disk_point = display_fields.index('DISK_INFO')
627 display_fields = (display_fields[:disk_point] + disk_fields +
628 display_fields[disk_point+1:])
630 main_status['memory'] += ' MiB'
631 for field, disp in display_fields:
632 if field in ('uptime', 'cputime') and locals()[field] is not None:
633 fields.append((disp, locals()[field]))
634 elif field in machine_info:
635 fields.append((disp, machine_info[field]))
636 elif field in main_status:
637 fields.append((disp, main_status[field]))
640 #fields.append((disp, None))
642 max_mem = validation.maxMemory(machine.owner, state, machine, False)
643 max_disk = validation.maxDisk(machine.owner, machine)
644 defaults = Defaults()
645 for name in 'machine_id name description administrator owner memory contact'.split():
646 if getattr(machine, name):
647 setattr(defaults, name, getattr(machine, name))
648 defaults.type = machine.type.type_id
649 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
650 d = dict(user=username,
651 on=status is not None,
662 def send_error_mail(subject, body):
665 to = config.web.errormail
671 """ % (to, config.web.hostname, subject, body)
672 p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
673 stdin=subprocess.PIPE)