2 """Main CGI script for web interface"""
15 from StringIO import StringIO
17 def revertStandardError():
18 """Move stderr to stdout, and return the contents of the old stderr."""
20 if not isinstance(errio, StringIO):
22 sys.stderr = sys.stdout
27 """Revert stderr to stdout, and print the contents of stderr"""
28 if isinstance(sys.stderr, StringIO):
29 print revertStandardError()
31 if __name__ == '__main__':
33 atexit.register(printError)
36 from Cheetah.Template import Template
37 import sipb_xen_database
38 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
41 from webcommon import InvalidInput, CodeError, State
43 from getafsgroups import getAfsGroupMembers
45 invirt_config = invirt.config.load()
48 if path.startswith('/'):
53 return path[:i], path[i:]
57 self.start_time = time.time()
60 def checkpoint(self, s):
61 self.checkpoints.append((s, time.time()))
64 return ('Timing info:\n%s\n' %
65 '\n'.join(['%s: %s' % (d, t - self.start_time) for
66 (d, t) in self.checkpoints]))
68 checkpoint = Checkpoint()
71 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
74 """Return HTML code for a (?) link to a specified help topic"""
75 return ('<span class="helplink"><a href="help?' +
76 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
77 +'" target="_blank" ' +
78 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
80 def makeErrorPre(old, addition):
84 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
86 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
88 Template.sipb_xen_database = sipb_xen_database
89 Template.helppopup = staticmethod(helppopup)
93 """Class to store a dictionary that will be converted to JSON"""
94 def __init__(self, **kws):
102 return simplejson.dumps(self.data)
104 def addError(self, text):
105 """Add stderr text to be displayed on the website."""
107 makeErrorPre(self.data.get('err'), text)
110 """Class to store default values for fields."""
119 def __init__(self, max_memory=None, max_disk=None, **kws):
120 if max_memory is not None:
121 self.memory = min(self.memory, max_memory)
122 if max_disk is not None:
123 self.max_disk = min(self.disk, max_disk)
125 setattr(self, key, kws[key])
129 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
131 def invalidInput(op, username, fields, err, emsg):
132 """Print an error page when an InvalidInput exception occurs"""
133 d = dict(op=op, user=username, err_field=err.err_field,
134 err_value=str(err.err_value), stderr=emsg,
135 errorMessage=str(err))
136 return templates.invalid(searchList=[d])
139 """Does the machine with a given status list support VNC?"""
143 if l[0] == 'device' and l[1][0] == 'vfb':
145 return 'location' in d
148 def parseCreate(username, state, fields):
149 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom autoinstall'.split()])
150 validate = validation.Validate(username, state, strict=True, **kws)
151 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
152 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
153 cdrom=getattr(validate, 'cdrom', None),
154 autoinstall=getattr(validate, 'autoinstall', None))
156 def create(username, state, path, fields):
157 """Handler for create requests."""
159 parsed_fields = parseCreate(username, state, fields)
160 machine = controls.createVm(username, state, **parsed_fields)
161 except InvalidInput, err:
165 state.clear() #Changed global state
166 d = getListDict(username, state)
169 for field in fields.keys():
170 setattr(d['defaults'], field, fields.getfirst(field))
172 d['new_machine'] = parsed_fields['name']
173 return templates.list(searchList=[d])
176 def getListDict(username, state):
177 """Gets the list of local variables used by list.tmpl."""
178 checkpoint.checkpoint('Starting')
179 machines = state.machines
180 checkpoint.checkpoint('Got my machines')
183 xmlist = state.xmlist
184 checkpoint.checkpoint('Got uptimes')
185 can_clone = 'ice3' not in state.xmlist_raw
191 m.uptime = xmlist[m]['uptime']
192 if xmlist[m]['console']:
197 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
198 max_memory = validation.maxMemory(username, state)
199 max_disk = validation.maxDisk(username)
200 checkpoint.checkpoint('Got max mem/disk')
201 defaults = Defaults(max_memory=max_memory,
205 checkpoint.checkpoint('Got defaults')
206 def sortkey(machine):
207 return (machine.owner != username, machine.owner, machine.name)
208 machines = sorted(machines, key=sortkey)
209 d = dict(user=username,
210 cant_add_vm=validation.cantAddVm(username, state),
211 max_memory=max_memory,
219 def listVms(username, state, path, fields):
220 """Handler for list requests."""
221 checkpoint.checkpoint('Getting list dict')
222 d = getListDict(username, state)
223 checkpoint.checkpoint('Got list dict')
224 return templates.list(searchList=[d])
226 def vnc(username, state, path, fields):
229 Note that due to same-domain restrictions, the applet connects to
230 the webserver, which needs to forward those requests to the xen
231 server. The Xen server runs another proxy that (1) authenticates
232 and (2) finds the correct port for the VM.
234 You might want iptables like:
236 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
237 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
238 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
239 --dport 10003 -j SNAT --to-source 18.187.7.142
240 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
241 --dport 10003 -j ACCEPT
243 Remember to enable iptables!
244 echo 1 > /proc/sys/net/ipv4/ip_forward
246 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
248 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
251 data["user"] = username
252 data["machine"] = machine.name
253 data["expires"] = time.time()+(5*60)
254 pickled_data = cPickle.dumps(data)
255 m = hmac.new(TOKEN_KEY, digestmod=sha)
256 m.update(pickled_data)
257 token = {'data': pickled_data, 'digest': m.digest()}
258 token = cPickle.dumps(token)
259 token = base64.urlsafe_b64encode(token)
260 host = controls.listHost(machine)
262 port = 10003 + [config_host["hostname"] for config_host in invirt_config["hosts"]
267 status = controls.statusInfo(machine)
268 has_vnc = hasVnc(status)
270 d = dict(user=username,
274 hostname=state.environ.get('SERVER_NAME', 'localhost'),
277 return templates.vnc(searchList=[d])
279 def getHostname(nic):
280 """Find the hostname associated with a NIC.
282 XXX this should be merged with the similar logic in DNS and DHCP.
284 if nic.hostname and '.' in nic.hostname:
287 return nic.machine.name + '.xvm.mit.edu'
292 def getNicInfo(data_dict, machine):
293 """Helper function for info, get data on nics for a machine.
295 Modifies data_dict to include the relevant data, and returns a list
296 of (key, name) pairs to display "name: data_dict[key]" to the user.
298 data_dict['num_nics'] = len(machine.nics)
299 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
300 ('nic%s_mac', 'NIC %s MAC Addr'),
301 ('nic%s_ip', 'NIC %s IP'),
304 for i in range(len(machine.nics)):
305 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
307 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
308 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
309 data_dict['nic%s_ip' % i] = machine.nics[i].ip
310 if len(machine.nics) == 1:
311 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
314 def getDiskInfo(data_dict, machine):
315 """Helper function for info, get data on disks for a machine.
317 Modifies data_dict to include the relevant data, and returns a list
318 of (key, name) pairs to display "name: data_dict[key]" to the user.
320 data_dict['num_disks'] = len(machine.disks)
321 disk_fields_template = [('%s_size', '%s size')]
323 for disk in machine.disks:
324 name = disk.guest_device_name
325 disk_fields.extend([(x % name, y % name) for x, y in
326 disk_fields_template])
327 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
330 def command(username, state, path, fields):
331 """Handler for running commands like boot and delete on a VM."""
332 back = fields.getfirst('back')
334 d = controls.commandResult(username, state, fields)
335 if d['command'] == 'Delete VM':
337 except InvalidInput, err:
340 print >> sys.stderr, err
345 return templates.command(searchList=[d])
347 state.clear() #Changed global state
348 d = getListDict(username, state)
350 return templates.list(searchList=[d])
352 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
353 return ({'Status': '303 See Other',
354 'Location': 'info?machine_id=%d' % machine.machine_id},
355 "You shouldn't see this message.")
357 raise InvalidInput('back', back, 'Not a known back page.')
359 def modifyDict(username, state, fields):
360 """Modify a machine as specified by CGI arguments.
362 Return a list of local variables for modify.tmpl.
365 transaction = ctx.current.create_transaction()
367 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
368 validate = validation.Validate(username, state, **kws)
369 machine = validate.machine
370 oldname = machine.name
372 if hasattr(validate, 'memory'):
373 machine.memory = validate.memory
375 if hasattr(validate, 'vmtype'):
376 machine.type = validate.vmtype
378 if hasattr(validate, 'disksize'):
379 disksize = validate.disksize
380 disk = machine.disks[0]
381 if disk.size != disksize:
382 olddisk[disk.guest_device_name] = disksize
384 ctx.current.save(disk)
387 if hasattr(validate, 'owner') and validate.owner != machine.owner:
388 machine.owner = validate.owner
390 if hasattr(validate, 'name'):
391 machine.name = validate.name
392 if hasattr(validate, 'description'):
393 machine.description = validate.description
394 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
395 machine.administrator = validate.admin
397 if hasattr(validate, 'contact'):
398 machine.contact = validate.contact
400 ctx.current.save(machine)
402 print >> sys.stderr, machine, machine.administrator
403 cache_acls.refreshMachine(machine)
406 transaction.rollback()
408 for diskname in olddisk:
409 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
410 if hasattr(validate, 'name'):
411 controls.renameMachine(machine, oldname, validate.name)
412 return dict(user=username,
416 def modify(username, state, path, fields):
417 """Handler for modifying attributes of a machine."""
419 modify_dict = modifyDict(username, state, fields)
420 except InvalidInput, err:
422 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
424 machine = modify_dict['machine']
427 info_dict = infoDict(username, state, machine)
428 info_dict['err'] = err
430 for field in fields.keys():
431 setattr(info_dict['defaults'], field, fields.getfirst(field))
432 info_dict['result'] = result
433 return templates.info(searchList=[info_dict])
436 def helpHandler(username, state, path, fields):
437 """Handler for help messages."""
438 simple = fields.getfirst('simple')
439 subjects = fields.getlist('subject')
441 help_mapping = {'ParaVM Console': """
442 ParaVM machines do not support local console access over VNC. To
443 access the serial console of these machines, you can SSH with Kerberos
444 to console.xvm.mit.edu, using the name of the machine as your
447 HVM machines use the virtualization features of the processor, while
448 ParaVM machines use Xen's emulation of virtualization features. You
449 want an HVM virtualized machine.""",
451 Don't ask us! We're as mystified as you are.""",
453 The owner field is used to determine <a
454 href="help?subject=Quotas">quotas</a>. It must be the name of a
455 locker that you are an AFS administrator of. In particular, you or an
456 AFS group you are a member of must have AFS rlidwka bits on the
457 locker. You can check who administers the LOCKER locker using the
458 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
459 href="help?subject=Administrator">administrator</a>.""",
461 The administrator field determines who can access the console and
462 power on and off the machine. This can be either a user or a moira
465 Quotas are determined on a per-locker basis. Each locker may have a
466 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
469 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
470 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
471 your machine will run just fine, but the applet's display of the
472 console will suffer artifacts.
477 subjects = sorted(help_mapping.keys())
479 d = dict(user=username,
482 mapping=help_mapping)
484 return templates.help(searchList=[d])
487 def badOperation(u, s, p, e):
488 """Function called when accessing an unknown URI."""
489 return ({'Status': '404 Not Found'}, 'Invalid operation.')
491 def infoDict(username, state, machine):
492 """Get the variables used by info.tmpl."""
493 status = controls.statusInfo(machine)
494 checkpoint.checkpoint('Getting status info')
495 has_vnc = hasVnc(status)
497 main_status = dict(name=machine.name,
498 memory=str(machine.memory))
502 main_status = dict(status[1:])
503 main_status['host'] = controls.listHost(machine)
504 start_time = float(main_status.get('start_time', 0))
505 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
506 cpu_time_float = float(main_status.get('cpu_time', 0))
507 cputime = datetime.timedelta(seconds=int(cpu_time_float))
508 checkpoint.checkpoint('Status')
509 display_fields = """name uptime memory state cpu_weight on_reboot
510 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
511 display_fields = [('name', 'Name'),
512 ('description', 'Description'),
514 ('administrator', 'Administrator'),
515 ('contact', 'Contact'),
518 ('uptime', 'uptime'),
519 ('cputime', 'CPU usage'),
520 ('host', 'Hosted on'),
523 ('state', 'state (xen format)'),
524 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
525 ('on_reboot', 'Action on VM reboot'),
526 ('on_poweroff', 'Action on VM poweroff'),
527 ('on_crash', 'Action on VM crash'),
528 ('on_xend_start', 'Action on Xen start'),
529 ('on_xend_stop', 'Action on Xen stop'),
530 ('bootloader', 'Bootloader options'),
534 machine_info['name'] = machine.name
535 machine_info['description'] = machine.description
536 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
537 machine_info['owner'] = machine.owner
538 machine_info['administrator'] = machine.administrator
539 machine_info['contact'] = machine.contact
541 nic_fields = getNicInfo(machine_info, machine)
542 nic_point = display_fields.index('NIC_INFO')
543 display_fields = (display_fields[:nic_point] + nic_fields +
544 display_fields[nic_point+1:])
546 disk_fields = getDiskInfo(machine_info, machine)
547 disk_point = display_fields.index('DISK_INFO')
548 display_fields = (display_fields[:disk_point] + disk_fields +
549 display_fields[disk_point+1:])
551 main_status['memory'] += ' MiB'
552 for field, disp in display_fields:
553 if field in ('uptime', 'cputime') and locals()[field] is not None:
554 fields.append((disp, locals()[field]))
555 elif field in machine_info:
556 fields.append((disp, machine_info[field]))
557 elif field in main_status:
558 fields.append((disp, main_status[field]))
561 #fields.append((disp, None))
563 checkpoint.checkpoint('Got fields')
566 max_mem = validation.maxMemory(machine.owner, state, machine, False)
567 checkpoint.checkpoint('Got mem')
568 max_disk = validation.maxDisk(machine.owner, machine)
569 defaults = Defaults()
570 for name in 'machine_id name description administrator owner memory contact'.split():
571 setattr(defaults, name, getattr(machine, name))
572 defaults.type = machine.type.type_id
573 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
574 checkpoint.checkpoint('Got defaults')
575 d = dict(user=username,
576 on=status is not None,
584 owner_help=helppopup("Owner"),
588 def info(username, state, path, fields):
589 """Handler for info on a single VM."""
590 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
591 d = infoDict(username, state, machine)
592 checkpoint.checkpoint('Got infodict')
593 return templates.info(searchList=[d])
595 def unauthFront(_, _2, _3, fields):
596 """Information for unauth'd users."""
597 return templates.unauth(searchList=[{'simple' : True}])
599 def overlord(username, state, path, fields):
601 return ({'Status': '303 See Other',
602 'Location': 'overlord/'},
603 "You shouldn't see this message.")
604 if not username in getAfsGroupMembers('system:xvm', 'athena.mit.edu'):
605 raise InvalidInput('username', username, 'Not an overlord.')
606 newstate = State(username, overlord=True)
607 newstate.environ = state.environ
608 return handler(username, newstate, path, fields)
610 def throwError(_, __, ___, ____):
611 """Throw an error, to test the error-tracing mechanisms."""
612 raise RuntimeError("test of the emergency broadcast system")
614 mapping = dict(list=listVms,
623 errortest=throwError)
625 def printHeaders(headers):
626 """Print a dictionary as HTTP headers."""
627 for key, value in headers.iteritems():
628 print '%s: %s' % (key, value)
631 def send_error_mail(subject, body):
636 From: root@xvm.mit.edu
640 """ % (to, subject, body)
641 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
646 def show_error(op, username, fields, err, emsg, traceback):
647 """Print an error page when an exception occurs"""
648 d = dict(op=op, user=username, fields=fields,
649 errorMessage=str(err), stderr=emsg, traceback=traceback)
650 details = templates.error_raw(searchList=[d])
651 if username not in ('price', 'ecprice', 'andersk'): #add yourself at will
652 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
654 d['details'] = details
655 return templates.error(searchList=[d])
657 def getUser(environ):
658 """Return the current user based on the SSL environment variables"""
659 return environ.get('REMOTE_USER', None)
661 def handler(username, state, path, fields):
662 operation, path = pathSplit(path)
665 print 'Starting', operation
666 fun = mapping.get(operation, badOperation)
667 return fun(username, state, path, fields)
670 def __init__(self, environ, start_response):
671 self.environ = environ
672 self.start = start_response
674 self.username = getUser(environ)
675 self.state = State(self.username)
676 self.state.environ = environ
681 start_time = time.time()
682 sipb_xen_database.clear_cache()
683 sys.stderr = StringIO()
684 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
685 operation = self.environ.get('PATH_INFO', '')
687 self.start("301 Moved Permanently", [('Location', './')])
689 if self.username is None:
693 checkpoint.checkpoint('Before')
694 output = handler(self.username, self.state, operation, fields)
695 checkpoint.checkpoint('After')
697 headers = dict(DEFAULT_HEADERS)
698 if isinstance(output, tuple):
699 new_headers, output = output
700 headers.update(new_headers)
701 e = revertStandardError()
703 if hasattr(output, 'addError'):
706 # This only happens on redirects, so it'd be a pain to get
707 # the message to the user. Maybe in the response is useful.
708 output = output + '\n\nstderr:\n' + e
709 output_string = str(output)
710 checkpoint.checkpoint('output as a string')
711 except Exception, err:
712 if not fields.has_key('js'):
713 if isinstance(err, InvalidInput):
714 self.start('200 OK', [('Content-Type', 'text/html')])
715 e = revertStandardError()
716 yield str(invalidInput(operation, self.username, fields,
720 self.start('500 Internal Server Error',
721 [('Content-Type', 'text/html')])
722 e = revertStandardError()
723 s = show_error(operation, self.username, fields,
724 err, e, traceback.format_exc())
727 status = headers.setdefault('Status', '200 OK')
728 del headers['Status']
729 self.start(status, headers.items())
731 if fields.has_key('timedebug'):
732 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
735 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
739 from flup.server.fcgi_fork import WSGIServer
740 WSGIServer(constructor()).run()
742 if __name__ == '__main__':