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 port = 10003 + [config_host["hostname"] for config_host in invirt_config["hosts"]
261 ].index(controls.listHost(machine))
263 status = controls.statusInfo(machine)
264 has_vnc = hasVnc(status)
266 d = dict(user=username,
270 hostname=state.environ.get('SERVER_NAME', 'localhost'),
273 return templates.vnc(searchList=[d])
275 def getHostname(nic):
276 """Find the hostname associated with a NIC.
278 XXX this should be merged with the similar logic in DNS and DHCP.
280 if nic.hostname and '.' in nic.hostname:
283 return nic.machine.name + '.xvm.mit.edu'
288 def getNicInfo(data_dict, machine):
289 """Helper function for info, get data on nics for a machine.
291 Modifies data_dict to include the relevant data, and returns a list
292 of (key, name) pairs to display "name: data_dict[key]" to the user.
294 data_dict['num_nics'] = len(machine.nics)
295 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
296 ('nic%s_mac', 'NIC %s MAC Addr'),
297 ('nic%s_ip', 'NIC %s IP'),
300 for i in range(len(machine.nics)):
301 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
303 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
304 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
305 data_dict['nic%s_ip' % i] = machine.nics[i].ip
306 if len(machine.nics) == 1:
307 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
310 def getDiskInfo(data_dict, machine):
311 """Helper function for info, get data on disks for a machine.
313 Modifies data_dict to include the relevant data, and returns a list
314 of (key, name) pairs to display "name: data_dict[key]" to the user.
316 data_dict['num_disks'] = len(machine.disks)
317 disk_fields_template = [('%s_size', '%s size')]
319 for disk in machine.disks:
320 name = disk.guest_device_name
321 disk_fields.extend([(x % name, y % name) for x, y in
322 disk_fields_template])
323 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
326 def command(username, state, path, fields):
327 """Handler for running commands like boot and delete on a VM."""
328 back = fields.getfirst('back')
330 d = controls.commandResult(username, state, fields)
331 if d['command'] == 'Delete VM':
333 except InvalidInput, err:
336 print >> sys.stderr, err
341 return templates.command(searchList=[d])
343 state.clear() #Changed global state
344 d = getListDict(username, state)
346 return templates.list(searchList=[d])
348 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
349 return ({'Status': '303 See Other',
350 'Location': 'info?machine_id=%d' % machine.machine_id},
351 "You shouldn't see this message.")
353 raise InvalidInput('back', back, 'Not a known back page.')
355 def modifyDict(username, state, fields):
356 """Modify a machine as specified by CGI arguments.
358 Return a list of local variables for modify.tmpl.
361 transaction = ctx.current.create_transaction()
363 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
364 validate = validation.Validate(username, state, **kws)
365 machine = validate.machine
366 oldname = machine.name
368 if hasattr(validate, 'memory'):
369 machine.memory = validate.memory
371 if hasattr(validate, 'vmtype'):
372 machine.type = validate.vmtype
374 if hasattr(validate, 'disksize'):
375 disksize = validate.disksize
376 disk = machine.disks[0]
377 if disk.size != disksize:
378 olddisk[disk.guest_device_name] = disksize
380 ctx.current.save(disk)
383 if hasattr(validate, 'owner') and validate.owner != machine.owner:
384 machine.owner = validate.owner
386 if hasattr(validate, 'name'):
387 machine.name = validate.name
388 if hasattr(validate, 'description'):
389 machine.description = validate.description
390 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
391 machine.administrator = validate.admin
393 if hasattr(validate, 'contact'):
394 machine.contact = validate.contact
396 ctx.current.save(machine)
398 print >> sys.stderr, machine, machine.administrator
399 cache_acls.refreshMachine(machine)
402 transaction.rollback()
404 for diskname in olddisk:
405 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
406 if hasattr(validate, 'name'):
407 controls.renameMachine(machine, oldname, validate.name)
408 return dict(user=username,
412 def modify(username, state, path, fields):
413 """Handler for modifying attributes of a machine."""
415 modify_dict = modifyDict(username, state, fields)
416 except InvalidInput, err:
418 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
420 machine = modify_dict['machine']
423 info_dict = infoDict(username, state, machine)
424 info_dict['err'] = err
426 for field in fields.keys():
427 setattr(info_dict['defaults'], field, fields.getfirst(field))
428 info_dict['result'] = result
429 return templates.info(searchList=[info_dict])
432 def helpHandler(username, state, path, fields):
433 """Handler for help messages."""
434 simple = fields.getfirst('simple')
435 subjects = fields.getlist('subject')
437 help_mapping = {'ParaVM Console': """
438 ParaVM machines do not support local console access over VNC. To
439 access the serial console of these machines, you can SSH with Kerberos
440 to console.xvm.mit.edu, using the name of the machine as your
443 HVM machines use the virtualization features of the processor, while
444 ParaVM machines use Xen's emulation of virtualization features. You
445 want an HVM virtualized machine.""",
447 Don't ask us! We're as mystified as you are.""",
449 The owner field is used to determine <a
450 href="help?subject=Quotas">quotas</a>. It must be the name of a
451 locker that you are an AFS administrator of. In particular, you or an
452 AFS group you are a member of must have AFS rlidwka bits on the
453 locker. You can check who administers the LOCKER locker using the
454 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
455 href="help?subject=Administrator">administrator</a>.""",
457 The administrator field determines who can access the console and
458 power on and off the machine. This can be either a user or a moira
461 Quotas are determined on a per-locker basis. Each locker may have a
462 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
465 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
466 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
467 your machine will run just fine, but the applet's display of the
468 console will suffer artifacts.
473 subjects = sorted(help_mapping.keys())
475 d = dict(user=username,
478 mapping=help_mapping)
480 return templates.help(searchList=[d])
483 def badOperation(u, s, p, e):
484 """Function called when accessing an unknown URI."""
485 return ({'Status': '404 Not Found'}, 'Invalid operation.')
487 def infoDict(username, state, machine):
488 """Get the variables used by info.tmpl."""
489 status = controls.statusInfo(machine)
490 checkpoint.checkpoint('Getting status info')
491 has_vnc = hasVnc(status)
493 main_status = dict(name=machine.name,
494 memory=str(machine.memory))
498 main_status = dict(status[1:])
499 main_status['host'] = controls.listHost(machine)
500 start_time = float(main_status.get('start_time', 0))
501 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
502 cpu_time_float = float(main_status.get('cpu_time', 0))
503 cputime = datetime.timedelta(seconds=int(cpu_time_float))
504 checkpoint.checkpoint('Status')
505 display_fields = """name uptime memory state cpu_weight on_reboot
506 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
507 display_fields = [('name', 'Name'),
508 ('description', 'Description'),
510 ('administrator', 'Administrator'),
511 ('contact', 'Contact'),
514 ('uptime', 'uptime'),
515 ('cputime', 'CPU usage'),
516 ('host', 'Hosted on'),
519 ('state', 'state (xen format)'),
520 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
521 ('on_reboot', 'Action on VM reboot'),
522 ('on_poweroff', 'Action on VM poweroff'),
523 ('on_crash', 'Action on VM crash'),
524 ('on_xend_start', 'Action on Xen start'),
525 ('on_xend_stop', 'Action on Xen stop'),
526 ('bootloader', 'Bootloader options'),
530 machine_info['name'] = machine.name
531 machine_info['description'] = machine.description
532 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
533 machine_info['owner'] = machine.owner
534 machine_info['administrator'] = machine.administrator
535 machine_info['contact'] = machine.contact
537 nic_fields = getNicInfo(machine_info, machine)
538 nic_point = display_fields.index('NIC_INFO')
539 display_fields = (display_fields[:nic_point] + nic_fields +
540 display_fields[nic_point+1:])
542 disk_fields = getDiskInfo(machine_info, machine)
543 disk_point = display_fields.index('DISK_INFO')
544 display_fields = (display_fields[:disk_point] + disk_fields +
545 display_fields[disk_point+1:])
547 main_status['memory'] += ' MiB'
548 for field, disp in display_fields:
549 if field in ('uptime', 'cputime') and locals()[field] is not None:
550 fields.append((disp, locals()[field]))
551 elif field in machine_info:
552 fields.append((disp, machine_info[field]))
553 elif field in main_status:
554 fields.append((disp, main_status[field]))
557 #fields.append((disp, None))
559 checkpoint.checkpoint('Got fields')
562 max_mem = validation.maxMemory(machine.owner, state, machine, False)
563 checkpoint.checkpoint('Got mem')
564 max_disk = validation.maxDisk(machine.owner, machine)
565 defaults = Defaults()
566 for name in 'machine_id name description administrator owner memory contact'.split():
567 setattr(defaults, name, getattr(machine, name))
568 defaults.type = machine.type.type_id
569 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
570 checkpoint.checkpoint('Got defaults')
571 d = dict(user=username,
572 on=status is not None,
580 owner_help=helppopup("Owner"),
584 def info(username, state, path, fields):
585 """Handler for info on a single VM."""
586 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
587 d = infoDict(username, state, machine)
588 checkpoint.checkpoint('Got infodict')
589 return templates.info(searchList=[d])
591 def unauthFront(_, _2, _3, fields):
592 """Information for unauth'd users."""
593 return templates.unauth(searchList=[{'simple' : True}])
595 def overlord(username, state, path, fields):
597 return ({'Status': '303 See Other',
598 'Location': 'overlord/'},
599 "You shouldn't see this message.")
600 if not username in getAfsGroupMembers('system:xvm', 'athena.mit.edu'):
601 raise InvalidInput('username', username, 'Not an overlord.')
602 newstate = State(username, overlord=True)
603 newstate.environ = state.environ
604 return handler(username, newstate, path, fields)
606 def throwError(_, __, ___, ____):
607 """Throw an error, to test the error-tracing mechanisms."""
608 raise RuntimeError("test of the emergency broadcast system")
610 mapping = dict(list=listVms,
619 errortest=throwError)
621 def printHeaders(headers):
622 """Print a dictionary as HTTP headers."""
623 for key, value in headers.iteritems():
624 print '%s: %s' % (key, value)
627 def send_error_mail(subject, body):
632 From: root@xvm.mit.edu
636 """ % (to, subject, body)
637 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
642 def show_error(op, username, fields, err, emsg, traceback):
643 """Print an error page when an exception occurs"""
644 d = dict(op=op, user=username, fields=fields,
645 errorMessage=str(err), stderr=emsg, traceback=traceback)
646 details = templates.error_raw(searchList=[d])
647 if username not in ('price', 'ecprice', 'andersk'): #add yourself at will
648 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
650 d['details'] = details
651 return templates.error(searchList=[d])
653 def getUser(environ):
654 """Return the current user based on the SSL environment variables"""
655 return environ.get('REMOTE_USER', None)
657 def handler(username, state, path, fields):
658 operation, path = pathSplit(path)
661 print 'Starting', operation
662 fun = mapping.get(operation, badOperation)
663 return fun(username, state, path, fields)
666 def __init__(self, environ, start_response):
667 self.environ = environ
668 self.start = start_response
670 self.username = getUser(environ)
671 self.state = State(self.username)
672 self.state.environ = environ
677 start_time = time.time()
678 sipb_xen_database.clear_cache()
679 sys.stderr = StringIO()
680 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
681 operation = self.environ.get('PATH_INFO', '')
683 self.start("301 Moved Permanently", [('Location', './')])
685 if self.username is None:
689 checkpoint.checkpoint('Before')
690 output = handler(self.username, self.state, operation, fields)
691 checkpoint.checkpoint('After')
693 headers = dict(DEFAULT_HEADERS)
694 if isinstance(output, tuple):
695 new_headers, output = output
696 headers.update(new_headers)
697 e = revertStandardError()
699 if hasattr(output, 'addError'):
702 # This only happens on redirects, so it'd be a pain to get
703 # the message to the user. Maybe in the response is useful.
704 output = output + '\n\nstderr:\n' + e
705 output_string = str(output)
706 checkpoint.checkpoint('output as a string')
707 except Exception, err:
708 if not fields.has_key('js'):
709 if isinstance(err, InvalidInput):
710 self.start('200 OK', [('Content-Type', 'text/html')])
711 e = revertStandardError()
712 yield str(invalidInput(operation, self.username, fields,
716 self.start('500 Internal Server Error',
717 [('Content-Type', 'text/html')])
718 e = revertStandardError()
719 s = show_error(operation, self.username, fields,
720 err, e, traceback.format_exc())
723 status = headers.setdefault('Status', '200 OK')
724 del headers['Status']
725 self.start(status, headers.items())
727 if fields.has_key('timedebug'):
728 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
731 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
735 from flup.server.fcgi_fork import WSGIServer
736 WSGIServer(constructor()).run()
738 if __name__ == '__main__':