2 """Main CGI script for web interface"""
14 from StringIO import StringIO
16 def revertStandardError():
17 """Move stderr to stdout, and return the contents of the old stderr."""
19 if not isinstance(errio, StringIO):
21 sys.stderr = sys.stdout
26 """Revert stderr to stdout, and print the contents of stderr"""
27 if isinstance(sys.stderr, StringIO):
28 print revertStandardError()
30 if __name__ == '__main__':
32 atexit.register(printError)
34 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
37 from Cheetah.Template import Template
38 import sipb_xen_database
39 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
42 from webcommon import InvalidInput, CodeError, State
47 self.start_time = time.time()
50 def checkpoint(self, s):
51 self.checkpoints.append((s, time.time()))
54 return ('Timing info:\n%s\n' %
55 '\n'.join(['%s: %s' % (d, t - self.start_time) for
56 (d, t) in self.checkpoints]))
58 checkpoint = Checkpoint()
61 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
64 """Return HTML code for a (?) link to a specified help topic"""
65 return ('<span class="helplink"><a href="help?' +
66 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
67 +'" target="_blank" ' +
68 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
70 def makeErrorPre(old, addition):
74 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
76 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
78 Template.sipb_xen_database = sipb_xen_database
79 Template.helppopup = staticmethod(helppopup)
83 """Class to store a dictionary that will be converted to JSON"""
84 def __init__(self, **kws):
92 return simplejson.dumps(self.data)
94 def addError(self, text):
95 """Add stderr text to be displayed on the website."""
97 makeErrorPre(self.data.get('err'), text)
100 """Class to store default values for fields."""
108 def __init__(self, max_memory=None, max_disk=None, **kws):
109 if max_memory is not None:
110 self.memory = min(self.memory, max_memory)
111 if max_disk is not None:
112 self.max_disk = min(self.disk, max_disk)
114 setattr(self, key, kws[key])
118 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
120 def invalidInput(op, username, fields, err, emsg):
121 """Print an error page when an InvalidInput exception occurs"""
122 d = dict(op=op, user=username, err_field=err.err_field,
123 err_value=str(err.err_value), stderr=emsg,
124 errorMessage=str(err))
125 return templates.invalid(searchList=[d])
128 """Does the machine with a given status list support VNC?"""
132 if l[0] == 'device' and l[1][0] == 'vfb':
134 return 'location' in d
137 def parseCreate(username, state, fields):
138 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name owner memory disksize vmtype cdrom clone_from'.split()])
139 validate = validation.Validate(username, state, strict=True, **kws)
140 return dict(contact=username, name=validate.name, memory=validate.memory,
141 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
142 cdrom=getattr(validate, 'cdrom', None),
143 clone_from=getattr(validate, 'clone_from', None))
145 def create(username, state, fields):
146 """Handler for create requests."""
148 parsed_fields = parseCreate(username, state, fields)
149 machine = controls.createVm(username, state, **parsed_fields)
150 except InvalidInput, err:
154 state.clear() #Changed global state
155 d = getListDict(username, state)
158 for field in fields.keys():
159 setattr(d['defaults'], field, fields.getfirst(field))
161 d['new_machine'] = parsed_fields['name']
162 return templates.list(searchList=[d])
165 def getListDict(username, state):
166 """Gets the list of local variables used by list.tmpl."""
167 checkpoint.checkpoint('Starting')
168 machines = state.machines
169 checkpoint.checkpoint('Got my machines')
172 xmlist = state.xmlist
173 checkpoint.checkpoint('Got uptimes')
174 can_clone = 'ice3' not in state.xmlist_raw
180 m.uptime = xmlist[m]['uptime']
181 if xmlist[m]['console']:
186 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
187 max_memory = validation.maxMemory(username, state)
188 max_disk = validation.maxDisk(username)
189 checkpoint.checkpoint('Got max mem/disk')
190 defaults = Defaults(max_memory=max_memory,
194 checkpoint.checkpoint('Got defaults')
195 def sortkey(machine):
196 return (machine.owner != username, machine.owner, machine.name)
197 machines = sorted(machines, key=sortkey)
198 d = dict(user=username,
199 cant_add_vm=validation.cantAddVm(username, state),
200 max_memory=max_memory,
208 def listVms(username, state, fields):
209 """Handler for list requests."""
210 checkpoint.checkpoint('Getting list dict')
211 d = getListDict(username, state)
212 checkpoint.checkpoint('Got list dict')
213 return templates.list(searchList=[d])
215 def vnc(username, state, fields):
218 Note that due to same-domain restrictions, the applet connects to
219 the webserver, which needs to forward those requests to the xen
220 server. The Xen server runs another proxy that (1) authenticates
221 and (2) finds the correct port for the VM.
223 You might want iptables like:
225 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
226 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
227 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
228 --dport 10003 -j SNAT --to-source 18.187.7.142
229 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
230 --dport 10003 -j ACCEPT
232 Remember to enable iptables!
233 echo 1 > /proc/sys/net/ipv4/ip_forward
235 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
237 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
240 data["user"] = username
241 data["machine"] = machine.name
242 data["expires"] = time.time()+(5*60)
243 pickled_data = cPickle.dumps(data)
244 m = hmac.new(TOKEN_KEY, digestmod=sha)
245 m.update(pickled_data)
246 token = {'data': pickled_data, 'digest': m.digest()}
247 token = cPickle.dumps(token)
248 token = base64.urlsafe_b64encode(token)
250 status = controls.statusInfo(machine)
251 has_vnc = hasVnc(status)
253 d = dict(user=username,
257 hostname=state.environ.get('SERVER_NAME', 'localhost'),
259 return templates.vnc(searchList=[d])
261 def getHostname(nic):
262 """Find the hostname associated with a NIC.
264 XXX this should be merged with the similar logic in DNS and DHCP.
266 if nic.hostname and '.' in nic.hostname:
269 return nic.machine.name + '.xvm.mit.edu'
274 def getNicInfo(data_dict, machine):
275 """Helper function for info, get data on nics for a machine.
277 Modifies data_dict to include the relevant data, and returns a list
278 of (key, name) pairs to display "name: data_dict[key]" to the user.
280 data_dict['num_nics'] = len(machine.nics)
281 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
282 ('nic%s_mac', 'NIC %s MAC Addr'),
283 ('nic%s_ip', 'NIC %s IP'),
286 for i in range(len(machine.nics)):
287 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
289 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
290 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
291 data_dict['nic%s_ip' % i] = machine.nics[i].ip
292 if len(machine.nics) == 1:
293 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
296 def getDiskInfo(data_dict, machine):
297 """Helper function for info, get data on disks for a machine.
299 Modifies data_dict to include the relevant data, and returns a list
300 of (key, name) pairs to display "name: data_dict[key]" to the user.
302 data_dict['num_disks'] = len(machine.disks)
303 disk_fields_template = [('%s_size', '%s size')]
305 for disk in machine.disks:
306 name = disk.guest_device_name
307 disk_fields.extend([(x % name, y % name) for x, y in
308 disk_fields_template])
309 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
312 def command(username, state, fields):
313 """Handler for running commands like boot and delete on a VM."""
314 back = fields.getfirst('back')
316 d = controls.commandResult(username, state, fields)
317 if d['command'] == 'Delete VM':
319 except InvalidInput, err:
322 print >> sys.stderr, err
327 return templates.command(searchList=[d])
329 state.clear() #Changed global state
330 d = getListDict(username, state)
332 return templates.list(searchList=[d])
334 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
335 return ({'Status': '303 See Other',
336 'Location': '/info?machine_id=%d' % machine.machine_id},
337 "You shouldn't see this message.")
339 raise InvalidInput('back', back, 'Not a known back page.')
341 def modifyDict(username, state, fields):
342 """Modify a machine as specified by CGI arguments.
344 Return a list of local variables for modify.tmpl.
347 transaction = ctx.current.create_transaction()
349 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name memory vmtype disksize'.split()])
350 validate = validation.Validate(username, state, **kws)
351 machine = validate.machine
352 oldname = machine.name
354 if hasattr(validate, 'memory'):
355 machine.memory = validate.memory
357 if hasattr(validate, 'vmtype'):
358 machine.type = validate.vmtype
360 if hasattr(validate, 'disksize'):
361 disksize = validate.disksize
362 disk = machine.disks[0]
363 if disk.size != disksize:
364 olddisk[disk.guest_device_name] = disksize
366 ctx.current.save(disk)
369 if hasattr(validate, 'owner') and validate.owner != machine.owner:
370 machine.owner = validate.owner
372 if hasattr(validate, 'name'):
373 machine.name = validate.name
374 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
375 machine.administrator = validate.admin
377 if hasattr(validate, 'contact'):
378 machine.contact = validate.contact
380 ctx.current.save(machine)
382 print >> sys.stderr, machine, machine.administrator
383 cache_acls.refreshMachine(machine)
386 transaction.rollback()
388 for diskname in olddisk:
389 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
390 if hasattr(validate, 'name'):
391 controls.renameMachine(machine, oldname, validate.name)
392 return dict(user=username,
396 def modify(username, state, fields):
397 """Handler for modifying attributes of a machine."""
399 modify_dict = modifyDict(username, state, fields)
400 except InvalidInput, err:
402 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
404 machine = modify_dict['machine']
407 info_dict = infoDict(username, state, machine)
408 info_dict['err'] = err
410 for field in fields.keys():
411 setattr(info_dict['defaults'], field, fields.getfirst(field))
412 info_dict['result'] = result
413 return templates.info(searchList=[info_dict])
416 def helpHandler(username, state, fields):
417 """Handler for help messages."""
418 simple = fields.getfirst('simple')
419 subjects = fields.getlist('subject')
421 help_mapping = {'ParaVM Console': """
422 ParaVM machines do not support local console access over VNC. To
423 access the serial console of these machines, you can SSH with Kerberos
424 to console.xvm.mit.edu, using the name of the machine as your
427 HVM machines use the virtualization features of the processor, while
428 ParaVM machines use Xen's emulation of virtualization features. You
429 want an HVM virtualized machine.""",
431 Don't ask us! We're as mystified as you are.""",
433 The owner field is used to determine <a
434 href="help?subject=Quotas">quotas</a>. It must be the name of a
435 locker that you are an AFS administrator of. In particular, you or an
436 AFS group you are a member of must have AFS rlidwka bits on the
437 locker. You can check who administers the LOCKER locker using the
438 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
439 href="help?subject=Administrator">administrator</a>.""",
441 The administrator field determines who can access the console and
442 power on and off the machine. This can be either a user or a moira
445 Quotas are determined on a per-locker basis. Each locker may have a
446 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
449 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
450 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
451 your machine will run just fine, but the applet's display of the
452 console will suffer artifacts.
457 subjects = sorted(help_mapping.keys())
459 d = dict(user=username,
462 mapping=help_mapping)
464 return templates.help(searchList=[d])
467 def badOperation(u, s, e):
468 """Function called when accessing an unknown URI."""
469 raise CodeError("Unknown operation")
471 def infoDict(username, state, machine):
472 """Get the variables used by info.tmpl."""
473 status = controls.statusInfo(machine)
474 checkpoint.checkpoint('Getting status info')
475 has_vnc = hasVnc(status)
477 main_status = dict(name=machine.name,
478 memory=str(machine.memory))
482 main_status = dict(status[1:])
483 start_time = float(main_status.get('start_time', 0))
484 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
485 cpu_time_float = float(main_status.get('cpu_time', 0))
486 cputime = datetime.timedelta(seconds=int(cpu_time_float))
487 checkpoint.checkpoint('Status')
488 display_fields = """name uptime memory state cpu_weight on_reboot
489 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
490 display_fields = [('name', 'Name'),
492 ('administrator', 'Administrator'),
493 ('contact', 'Contact'),
496 ('uptime', 'uptime'),
497 ('cputime', 'CPU usage'),
500 ('state', 'state (xen format)'),
501 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
502 ('on_reboot', 'Action on VM reboot'),
503 ('on_poweroff', 'Action on VM poweroff'),
504 ('on_crash', 'Action on VM crash'),
505 ('on_xend_start', 'Action on Xen start'),
506 ('on_xend_stop', 'Action on Xen stop'),
507 ('bootloader', 'Bootloader options'),
511 machine_info['name'] = machine.name
512 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
513 machine_info['owner'] = machine.owner
514 machine_info['administrator'] = machine.administrator
515 machine_info['contact'] = machine.contact
517 nic_fields = getNicInfo(machine_info, machine)
518 nic_point = display_fields.index('NIC_INFO')
519 display_fields = (display_fields[:nic_point] + nic_fields +
520 display_fields[nic_point+1:])
522 disk_fields = getDiskInfo(machine_info, machine)
523 disk_point = display_fields.index('DISK_INFO')
524 display_fields = (display_fields[:disk_point] + disk_fields +
525 display_fields[disk_point+1:])
527 main_status['memory'] += ' MiB'
528 for field, disp in display_fields:
529 if field in ('uptime', 'cputime') and locals()[field] is not None:
530 fields.append((disp, locals()[field]))
531 elif field in machine_info:
532 fields.append((disp, machine_info[field]))
533 elif field in main_status:
534 fields.append((disp, main_status[field]))
537 #fields.append((disp, None))
539 checkpoint.checkpoint('Got fields')
542 max_mem = validation.maxMemory(machine.owner, state, machine, False)
543 checkpoint.checkpoint('Got mem')
544 max_disk = validation.maxDisk(machine.owner, machine)
545 defaults = Defaults()
546 for name in 'machine_id name administrator owner memory contact'.split():
547 setattr(defaults, name, getattr(machine, name))
548 defaults.type = machine.type.type_id
549 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
550 checkpoint.checkpoint('Got defaults')
551 d = dict(user=username,
552 on=status is not None,
560 owner_help=helppopup("Owner"),
564 def info(username, state, fields):
565 """Handler for info on a single VM."""
566 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
567 d = infoDict(username, state, machine)
568 checkpoint.checkpoint('Got infodict')
569 return templates.info(searchList=[d])
571 def unauthFront(_, _2, fields):
572 """Information for unauth'd users."""
573 return templates.unauth(searchList=[{'simple' : True}])
575 def throwError(_, __, ___):
576 """Throw an error, to test the error-tracing mechanisms."""
577 raise RuntimeError("test of the emergency broadcast system")
579 mapping = dict(list=listVms,
587 errortest=throwError)
589 def printHeaders(headers):
590 """Print a dictionary as HTTP headers."""
591 for key, value in headers.iteritems():
592 print '%s: %s' % (key, value)
595 def send_error_mail(subject, body):
600 From: root@xvm.mit.edu
604 """ % (to, subject, body)
605 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
610 def show_error(op, username, fields, err, emsg, traceback):
611 """Print an error page when an exception occurs"""
612 d = dict(op=op, user=username, fields=fields,
613 errorMessage=str(err), stderr=emsg, traceback=traceback)
614 details = templates.error_raw(searchList=[d])
615 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
617 d['details'] = details
618 return templates.error(searchList=[d])
620 def getUser(environ):
621 """Return the current user based on the SSL environment variables"""
622 email = environ.get('SSL_CLIENT_S_DN_Email', None)
625 if not email.endswith('@MIT.EDU'):
630 def __init__(self, environ, start_response):
631 self.environ = environ
632 self.start = start_response
634 self.username = getUser(environ)
635 self.state = State(self.username)
636 self.state.environ = environ
639 sys.stderr = StringIO()
640 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
641 operation = self.environ.get('PATH_INFO', '')
643 self.start("301 Moved Permanently", [('Location',
644 self.environ['SCRIPT_NAME']+'/')])
646 if self.username is None:
648 if operation.startswith('/'):
649 operation = operation[1:]
652 print 'Starting', operation
654 start_time = time.time()
655 fun = mapping.get(operation, badOperation)
657 checkpoint.checkpoint('Before')
658 output = fun(self.username, self.state, fields)
659 checkpoint.checkpoint('After')
661 headers = dict(DEFAULT_HEADERS)
662 if isinstance(output, tuple):
663 new_headers, output = output
664 headers.update(new_headers)
665 e = revertStandardError()
667 if isinstance(output, basestring):
668 sys.stderr = StringIO()
670 print >> sys.stderr, x
671 print >> sys.stderr, 'XXX'
672 print >> sys.stderr, e
675 output_string = str(output)
676 checkpoint.checkpoint('output as a string')
677 except Exception, err:
678 if not fields.has_key('js'):
679 if isinstance(err, InvalidInput):
680 self.start('200 OK', [('Content-Type', 'text/html')])
681 e = revertStandardError()
682 yield str(invalidInput(operation, self.username, fields,
686 self.start('500 Internal Server Error',
687 [('Content-Type', 'text/html')])
688 e = revertStandardError()
689 s = show_error(operation, self.username, fields,
690 err, e, traceback.format_exc())
693 status = headers.setdefault('Status', '200 OK')
694 del headers['Status']
695 self.start(status, headers.items())
697 if fields.has_key('timedebug'):
698 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
701 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
705 from flup.server.fcgi_fork import WSGIServer
706 WSGIServer(constructor()).run()
708 if __name__ == '__main__':