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."""
109 def __init__(self, max_memory=None, max_disk=None, **kws):
110 if max_memory is not None:
111 self.memory = min(self.memory, max_memory)
112 if max_disk is not None:
113 self.max_disk = min(self.disk, max_disk)
115 setattr(self, key, kws[key])
119 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
121 def invalidInput(op, username, fields, err, emsg):
122 """Print an error page when an InvalidInput exception occurs"""
123 d = dict(op=op, user=username, err_field=err.err_field,
124 err_value=str(err.err_value), stderr=emsg,
125 errorMessage=str(err))
126 return templates.invalid(searchList=[d])
129 """Does the machine with a given status list support VNC?"""
133 if l[0] == 'device' and l[1][0] == 'vfb':
135 return 'location' in d
138 def parseCreate(username, state, fields):
139 kws = dict([(kw, fields.getfirst(kw)) for kw in 'name description owner memory disksize vmtype cdrom clone_from'.split()])
140 validate = validation.Validate(username, state, strict=True, **kws)
141 return dict(contact=username, name=validate.name, description=validate.description, memory=validate.memory,
142 disksize=validate.disksize, owner=validate.owner, machine_type=validate.vmtype,
143 cdrom=getattr(validate, 'cdrom', None),
144 clone_from=getattr(validate, 'clone_from', None))
146 def create(username, state, fields):
147 """Handler for create requests."""
149 parsed_fields = parseCreate(username, state, fields)
150 machine = controls.createVm(username, state, **parsed_fields)
151 except InvalidInput, err:
155 state.clear() #Changed global state
156 d = getListDict(username, state)
159 for field in fields.keys():
160 setattr(d['defaults'], field, fields.getfirst(field))
162 d['new_machine'] = parsed_fields['name']
163 return templates.list(searchList=[d])
166 def getListDict(username, state):
167 """Gets the list of local variables used by list.tmpl."""
168 checkpoint.checkpoint('Starting')
169 machines = state.machines
170 checkpoint.checkpoint('Got my machines')
173 xmlist = state.xmlist
174 checkpoint.checkpoint('Got uptimes')
175 can_clone = 'ice3' not in state.xmlist_raw
181 m.uptime = xmlist[m]['uptime']
182 if xmlist[m]['console']:
187 has_vnc[m] = "ParaVM"+helppopup("ParaVM Console")
188 max_memory = validation.maxMemory(username, state)
189 max_disk = validation.maxDisk(username)
190 checkpoint.checkpoint('Got max mem/disk')
191 defaults = Defaults(max_memory=max_memory,
195 checkpoint.checkpoint('Got defaults')
196 def sortkey(machine):
197 return (machine.owner != username, machine.owner, machine.name)
198 machines = sorted(machines, key=sortkey)
199 d = dict(user=username,
200 cant_add_vm=validation.cantAddVm(username, state),
201 max_memory=max_memory,
209 def listVms(username, state, fields):
210 """Handler for list requests."""
211 checkpoint.checkpoint('Getting list dict')
212 d = getListDict(username, state)
213 checkpoint.checkpoint('Got list dict')
214 return templates.list(searchList=[d])
216 def vnc(username, state, fields):
219 Note that due to same-domain restrictions, the applet connects to
220 the webserver, which needs to forward those requests to the xen
221 server. The Xen server runs another proxy that (1) authenticates
222 and (2) finds the correct port for the VM.
224 You might want iptables like:
226 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
227 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
228 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
229 --dport 10003 -j SNAT --to-source 18.187.7.142
230 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
231 --dport 10003 -j ACCEPT
233 Remember to enable iptables!
234 echo 1 > /proc/sys/net/ipv4/ip_forward
236 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
238 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
241 data["user"] = username
242 data["machine"] = machine.name
243 data["expires"] = time.time()+(5*60)
244 pickled_data = cPickle.dumps(data)
245 m = hmac.new(TOKEN_KEY, digestmod=sha)
246 m.update(pickled_data)
247 token = {'data': pickled_data, 'digest': m.digest()}
248 token = cPickle.dumps(token)
249 token = base64.urlsafe_b64encode(token)
251 status = controls.statusInfo(machine)
252 has_vnc = hasVnc(status)
254 d = dict(user=username,
258 hostname=state.environ.get('SERVER_NAME', 'localhost'),
260 return templates.vnc(searchList=[d])
262 def getHostname(nic):
263 """Find the hostname associated with a NIC.
265 XXX this should be merged with the similar logic in DNS and DHCP.
267 if nic.hostname and '.' in nic.hostname:
270 return nic.machine.name + '.xvm.mit.edu'
275 def getNicInfo(data_dict, machine):
276 """Helper function for info, get data on nics for a machine.
278 Modifies data_dict to include the relevant data, and returns a list
279 of (key, name) pairs to display "name: data_dict[key]" to the user.
281 data_dict['num_nics'] = len(machine.nics)
282 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
283 ('nic%s_mac', 'NIC %s MAC Addr'),
284 ('nic%s_ip', 'NIC %s IP'),
287 for i in range(len(machine.nics)):
288 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
290 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
291 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
292 data_dict['nic%s_ip' % i] = machine.nics[i].ip
293 if len(machine.nics) == 1:
294 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
297 def getDiskInfo(data_dict, machine):
298 """Helper function for info, get data on disks for a machine.
300 Modifies data_dict to include the relevant data, and returns a list
301 of (key, name) pairs to display "name: data_dict[key]" to the user.
303 data_dict['num_disks'] = len(machine.disks)
304 disk_fields_template = [('%s_size', '%s size')]
306 for disk in machine.disks:
307 name = disk.guest_device_name
308 disk_fields.extend([(x % name, y % name) for x, y in
309 disk_fields_template])
310 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
313 def command(username, state, fields):
314 """Handler for running commands like boot and delete on a VM."""
315 back = fields.getfirst('back')
317 d = controls.commandResult(username, state, fields)
318 if d['command'] == 'Delete VM':
320 except InvalidInput, err:
323 print >> sys.stderr, err
328 return templates.command(searchList=[d])
330 state.clear() #Changed global state
331 d = getListDict(username, state)
333 return templates.list(searchList=[d])
335 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
336 return ({'Status': '303 See Other',
337 'Location': '/info?machine_id=%d' % machine.machine_id},
338 "You shouldn't see this message.")
340 raise InvalidInput('back', back, 'Not a known back page.')
342 def modifyDict(username, state, fields):
343 """Modify a machine as specified by CGI arguments.
345 Return a list of local variables for modify.tmpl.
348 transaction = ctx.current.create_transaction()
350 kws = dict([(kw, fields.getfirst(kw)) for kw in 'machine_id owner admin contact name description memory vmtype disksize'.split()])
351 validate = validation.Validate(username, state, **kws)
352 machine = validate.machine
353 oldname = machine.name
355 if hasattr(validate, 'memory'):
356 machine.memory = validate.memory
358 if hasattr(validate, 'vmtype'):
359 machine.type = validate.vmtype
361 if hasattr(validate, 'disksize'):
362 disksize = validate.disksize
363 disk = machine.disks[0]
364 if disk.size != disksize:
365 olddisk[disk.guest_device_name] = disksize
367 ctx.current.save(disk)
370 if hasattr(validate, 'owner') and validate.owner != machine.owner:
371 machine.owner = validate.owner
373 if hasattr(validate, 'name'):
374 machine.name = validate.name
375 if hasattr(validate, 'description'):
376 machine.description = validate.description
377 if hasattr(validate, 'admin') and validate.admin != machine.administrator:
378 machine.administrator = validate.admin
380 if hasattr(validate, 'contact'):
381 machine.contact = validate.contact
383 ctx.current.save(machine)
385 print >> sys.stderr, machine, machine.administrator
386 cache_acls.refreshMachine(machine)
389 transaction.rollback()
391 for diskname in olddisk:
392 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
393 if hasattr(validate, 'name'):
394 controls.renameMachine(machine, oldname, validate.name)
395 return dict(user=username,
399 def modify(username, state, fields):
400 """Handler for modifying attributes of a machine."""
402 modify_dict = modifyDict(username, state, fields)
403 except InvalidInput, err:
405 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
407 machine = modify_dict['machine']
410 info_dict = infoDict(username, state, machine)
411 info_dict['err'] = err
413 for field in fields.keys():
414 setattr(info_dict['defaults'], field, fields.getfirst(field))
415 info_dict['result'] = result
416 return templates.info(searchList=[info_dict])
419 def helpHandler(username, state, fields):
420 """Handler for help messages."""
421 simple = fields.getfirst('simple')
422 subjects = fields.getlist('subject')
424 help_mapping = {'ParaVM Console': """
425 ParaVM machines do not support local console access over VNC. To
426 access the serial console of these machines, you can SSH with Kerberos
427 to console.xvm.mit.edu, using the name of the machine as your
430 HVM machines use the virtualization features of the processor, while
431 ParaVM machines use Xen's emulation of virtualization features. You
432 want an HVM virtualized machine.""",
434 Don't ask us! We're as mystified as you are.""",
436 The owner field is used to determine <a
437 href="help?subject=Quotas">quotas</a>. It must be the name of a
438 locker that you are an AFS administrator of. In particular, you or an
439 AFS group you are a member of must have AFS rlidwka bits on the
440 locker. You can check who administers the LOCKER locker using the
441 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
442 href="help?subject=Administrator">administrator</a>.""",
444 The administrator field determines who can access the console and
445 power on and off the machine. This can be either a user or a moira
448 Quotas are determined on a per-locker basis. Each locker may have a
449 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
452 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
453 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
454 your machine will run just fine, but the applet's display of the
455 console will suffer artifacts.
460 subjects = sorted(help_mapping.keys())
462 d = dict(user=username,
465 mapping=help_mapping)
467 return templates.help(searchList=[d])
470 def badOperation(u, s, e):
471 """Function called when accessing an unknown URI."""
472 return ({'Status': '404 Not Found'}, 'Invalid operation.')
474 def infoDict(username, state, machine):
475 """Get the variables used by info.tmpl."""
476 status = controls.statusInfo(machine)
477 checkpoint.checkpoint('Getting status info')
478 has_vnc = hasVnc(status)
480 main_status = dict(name=machine.name,
481 memory=str(machine.memory))
485 main_status = dict(status[1:])
486 start_time = float(main_status.get('start_time', 0))
487 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
488 cpu_time_float = float(main_status.get('cpu_time', 0))
489 cputime = datetime.timedelta(seconds=int(cpu_time_float))
490 checkpoint.checkpoint('Status')
491 display_fields = """name uptime memory state cpu_weight on_reboot
492 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
493 display_fields = [('name', 'Name'),
494 ('description', 'Description'),
496 ('administrator', 'Administrator'),
497 ('contact', 'Contact'),
500 ('uptime', 'uptime'),
501 ('cputime', 'CPU usage'),
504 ('state', 'state (xen format)'),
505 ('cpu_weight', 'CPU weight'+helppopup('CPU Weight')),
506 ('on_reboot', 'Action on VM reboot'),
507 ('on_poweroff', 'Action on VM poweroff'),
508 ('on_crash', 'Action on VM crash'),
509 ('on_xend_start', 'Action on Xen start'),
510 ('on_xend_stop', 'Action on Xen stop'),
511 ('bootloader', 'Bootloader options'),
515 machine_info['name'] = machine.name
516 machine_info['description'] = machine.description
517 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
518 machine_info['owner'] = machine.owner
519 machine_info['administrator'] = machine.administrator
520 machine_info['contact'] = machine.contact
522 nic_fields = getNicInfo(machine_info, machine)
523 nic_point = display_fields.index('NIC_INFO')
524 display_fields = (display_fields[:nic_point] + nic_fields +
525 display_fields[nic_point+1:])
527 disk_fields = getDiskInfo(machine_info, machine)
528 disk_point = display_fields.index('DISK_INFO')
529 display_fields = (display_fields[:disk_point] + disk_fields +
530 display_fields[disk_point+1:])
532 main_status['memory'] += ' MiB'
533 for field, disp in display_fields:
534 if field in ('uptime', 'cputime') and locals()[field] is not None:
535 fields.append((disp, locals()[field]))
536 elif field in machine_info:
537 fields.append((disp, machine_info[field]))
538 elif field in main_status:
539 fields.append((disp, main_status[field]))
542 #fields.append((disp, None))
544 checkpoint.checkpoint('Got fields')
547 max_mem = validation.maxMemory(machine.owner, state, machine, False)
548 checkpoint.checkpoint('Got mem')
549 max_disk = validation.maxDisk(machine.owner, machine)
550 defaults = Defaults()
551 for name in 'machine_id name description administrator owner memory contact'.split():
552 setattr(defaults, name, getattr(machine, name))
553 defaults.type = machine.type.type_id
554 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
555 checkpoint.checkpoint('Got defaults')
556 d = dict(user=username,
557 on=status is not None,
565 owner_help=helppopup("Owner"),
569 def info(username, state, fields):
570 """Handler for info on a single VM."""
571 machine = validation.Validate(username, state, machine_id=fields.getfirst('machine_id')).machine
572 d = infoDict(username, state, machine)
573 checkpoint.checkpoint('Got infodict')
574 return templates.info(searchList=[d])
576 def unauthFront(_, _2, fields):
577 """Information for unauth'd users."""
578 return templates.unauth(searchList=[{'simple' : True}])
580 def throwError(_, __, ___):
581 """Throw an error, to test the error-tracing mechanisms."""
582 raise RuntimeError("test of the emergency broadcast system")
584 mapping = dict(list=listVms,
592 errortest=throwError)
594 def printHeaders(headers):
595 """Print a dictionary as HTTP headers."""
596 for key, value in headers.iteritems():
597 print '%s: %s' % (key, value)
600 def send_error_mail(subject, body):
605 From: root@xvm.mit.edu
609 """ % (to, subject, body)
610 p = subprocess.Popen(['/usr/sbin/sendmail', to], stdin=subprocess.PIPE)
615 def show_error(op, username, fields, err, emsg, traceback):
616 """Print an error page when an exception occurs"""
617 d = dict(op=op, user=username, fields=fields,
618 errorMessage=str(err), stderr=emsg, traceback=traceback)
619 details = templates.error_raw(searchList=[d])
620 send_error_mail('xvm error on %s for %s: %s' % (op, username, err),
622 d['details'] = details
623 return templates.error(searchList=[d])
625 def getUser(environ):
626 """Return the current user based on the SSL environment variables"""
627 email = environ.get('SSL_CLIENT_S_DN_Email', None)
630 if not email.endswith('@MIT.EDU'):
635 def __init__(self, environ, start_response):
636 self.environ = environ
637 self.start = start_response
639 self.username = getUser(environ)
640 self.state = State(self.username)
641 self.state.environ = environ
644 sipb_xen_database.clear_cache()
645 sys.stderr = StringIO()
646 fields = cgi.FieldStorage(fp=self.environ['wsgi.input'], environ=self.environ)
647 operation = self.environ.get('PATH_INFO', '')
649 self.start("301 Moved Permanently", [('Location',
650 self.environ['SCRIPT_NAME']+'/')])
652 if self.username is None:
654 if operation.startswith('/'):
655 operation = operation[1:]
658 print 'Starting', operation
660 start_time = time.time()
661 fun = mapping.get(operation, badOperation)
663 checkpoint.checkpoint('Before')
664 output = fun(self.username, self.state, fields)
665 checkpoint.checkpoint('After')
667 headers = dict(DEFAULT_HEADERS)
668 if isinstance(output, tuple):
669 new_headers, output = output
670 headers.update(new_headers)
671 e = revertStandardError()
673 if isinstance(output, basestring):
674 sys.stderr = StringIO()
676 print >> sys.stderr, x
677 print >> sys.stderr, 'XXX'
678 print >> sys.stderr, e
681 output_string = str(output)
682 checkpoint.checkpoint('output as a string')
683 except Exception, err:
684 if not fields.has_key('js'):
685 if isinstance(err, InvalidInput):
686 self.start('200 OK', [('Content-Type', 'text/html')])
687 e = revertStandardError()
688 yield str(invalidInput(operation, self.username, fields,
692 self.start('500 Internal Server Error',
693 [('Content-Type', 'text/html')])
694 e = revertStandardError()
695 s = show_error(operation, self.username, fields,
696 err, e, traceback.format_exc())
699 status = headers.setdefault('Status', '200 OK')
700 del headers['Status']
701 self.start(status, headers.items())
703 if fields.has_key('timedebug'):
704 yield '<pre>%s</pre>' % cgi.escape(str(checkpoint))
707 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
711 from flup.server.fcgi_fork import WSGIServer
712 WSGIServer(constructor()).run()
714 if __name__ == '__main__':