2 """Main CGI script for web interface"""
14 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)
34 sys.stderr = StringIO()
36 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
39 from Cheetah.Template import Template
40 from sipb_xen_database import Machine, CDROM, ctx, connect
42 from webcommon import InvalidInput, CodeError, g
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()
62 """Return HTML code for a (?) link to a specified help topic"""
63 return ('<span class="helplink"><a href="help?subject=' + subj +
64 '&simple=true" target="_blank" ' +
65 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
67 def makeErrorPre(old, addition):
71 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
73 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
75 Template.helppopup = staticmethod(helppopup)
79 """Class to store a dictionary that will be converted to JSON"""
80 def __init__(self, **kws):
88 return simplejson.dumps(self.data)
90 def addError(self, text):
91 """Add stderr text to be displayed on the website."""
93 makeErrorPre(self.data.get('err'), text)
96 """Class to store default values for fields."""
102 def __init__(self, max_memory=None, max_disk=None, **kws):
103 if max_memory is not None:
104 self.memory = min(self.memory, max_memory)
105 if max_disk is not None:
106 self.max_disk = min(self.disk, max_disk)
108 setattr(self, key, kws[key])
112 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
114 def error(op, user, fields, err, emsg):
115 """Print an error page when a CodeError occurs"""
116 d = dict(op=op, user=user, errorMessage=str(err),
118 return templates.error(searchList=[d])
120 def invalidInput(op, user, fields, err, emsg):
121 """Print an error page when an InvalidInput exception occurs"""
122 d = dict(op=op, user=user, 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(user, fields):
138 name = fields.getfirst('name')
139 if not validation.validMachineName(name):
140 raise InvalidInput('name', name, 'You must provide a machine name.')
143 if Machine.get_by(name=name):
144 raise InvalidInput('name', name,
145 "Name already exists.")
147 owner = validation.testOwner(user, fields.getfirst('owner'))
149 memory = fields.getfirst('memory')
150 memory = validation.validMemory(user, memory, on=True)
152 disk_size = fields.getfirst('disk')
153 disk_size = validation.validDisk(user, disk_size)
155 vm_type = fields.getfirst('vmtype')
156 if vm_type not in ('hvm', 'paravm'):
157 raise CodeError("Invalid vm type '%s'" % vm_type)
158 is_hvm = (vm_type == 'hvm')
160 cdrom = fields.getfirst('cdrom')
161 if cdrom is not None and not CDROM.get(cdrom):
162 raise CodeError("Invalid cdrom type '%s'" % cdrom)
163 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
164 owner=owner, is_hvm=is_hvm, cdrom=cdrom)
166 def create(user, fields):
167 """Handler for create requests."""
169 parsed_fields = parseCreate(user, fields)
170 machine = controls.createVm(**parsed_fields)
171 except InvalidInput, err:
175 g.clear() #Changed global state
176 d = getListDict(user)
179 for field in fields.keys():
180 setattr(d['defaults'], field, fields.getfirst(field))
182 d['new_machine'] = parsed_fields['name']
183 return templates.list(searchList=[d])
186 def getListDict(user):
187 machines = [m for m in Machine.select()
188 if validation.haveAccess(user, m)]
190 # machines = Machine.select()
192 # machines = Machine.query().join('users').filter_by(user=user).all()
193 checkpoint.checkpoint('Got my machines')
197 checkpoint.checkpoint('Got uptimes')
199 m.uptime = g.uptimes.get(m)
205 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
206 max_memory = validation.maxMemory(user)
207 max_disk = validation.maxDisk(user)
208 checkpoint.checkpoint('Got max mem/disk')
209 defaults = Defaults(max_memory=max_memory,
213 checkpoint.checkpoint('Got defaults')
215 cant_add_vm=validation.cantAddVm(user),
216 max_memory=max_memory,
222 cdroms=CDROM.select())
225 def listVms(user, fields):
226 """Handler for list requests."""
227 checkpoint.checkpoint('Getting list dict')
228 d = getListDict(user)
229 checkpoint.checkpoint('Got list dict')
230 return templates.list(searchList=[d])
232 def vnc(user, fields):
235 Note that due to same-domain restrictions, the applet connects to
236 the webserver, which needs to forward those requests to the xen
237 server. The Xen server runs another proxy that (1) authenticates
238 and (2) finds the correct port for the VM.
240 You might want iptables like:
242 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
243 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
244 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
245 --dport 10003 -j SNAT --to-source 18.187.7.142
246 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
247 --dport 10003 -j ACCEPT
249 Remember to enable iptables!
250 echo 1 > /proc/sys/net/ipv4/ip_forward
252 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
254 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
258 data["machine"] = machine.name
259 data["expires"] = time.time()+(5*60)
260 pickled_data = cPickle.dumps(data)
261 m = hmac.new(TOKEN_KEY, digestmod=sha)
262 m.update(pickled_data)
263 token = {'data': pickled_data, 'digest': m.digest()}
264 token = cPickle.dumps(token)
265 token = base64.urlsafe_b64encode(token)
267 status = controls.statusInfo(machine)
268 has_vnc = hasVnc(status)
274 hostname=os.environ.get('SERVER_NAME', 'localhost'),
276 return templates.vnc(searchList=[d])
278 def getNicInfo(data_dict, machine):
279 """Helper function for info, get data on nics for a machine.
281 Modifies data_dict to include the relevant data, and returns a list
282 of (key, name) pairs to display "name: data_dict[key]" to the user.
284 data_dict['num_nics'] = len(machine.nics)
285 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
286 ('nic%s_mac', 'NIC %s MAC Addr'),
287 ('nic%s_ip', 'NIC %s IP'),
290 for i in range(len(machine.nics)):
291 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
293 data_dict['nic%s_hostname' % i] = (machine.name +
294 '.servers.csail.mit.edu')
295 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
296 data_dict['nic%s_ip' % i] = machine.nics[i].ip
297 if len(machine.nics) == 1:
298 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
301 def getDiskInfo(data_dict, machine):
302 """Helper function for info, get data on disks for a machine.
304 Modifies data_dict to include the relevant data, and returns a list
305 of (key, name) pairs to display "name: data_dict[key]" to the user.
307 data_dict['num_disks'] = len(machine.disks)
308 disk_fields_template = [('%s_size', '%s size')]
310 for disk in machine.disks:
311 name = disk.guest_device_name
312 disk_fields.extend([(x % name, y % name) for x, y in
313 disk_fields_template])
314 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
317 def command(user, fields):
318 """Handler for running commands like boot and delete on a VM."""
319 back = fields.getfirst('back')
321 d = controls.commandResult(user, fields)
322 if d['command'] == 'Delete VM':
324 except InvalidInput, err:
327 print >> sys.stderr, err
332 return templates.command(searchList=[d])
334 g.clear() #Changed global state
335 d = getListDict(user)
337 return templates.list(searchList=[d])
339 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
340 d = infoDict(user, machine)
342 return templates.info(searchList=[d])
345 ('back', back, 'Not a known back page.')
347 def modifyDict(user, fields):
349 transaction = ctx.current.create_transaction()
351 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
352 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
353 admin = validation.testAdmin(user, fields.getfirst('administrator'),
355 contact = validation.testContact(user, fields.getfirst('contact'),
357 name = validation.testName(user, fields.getfirst('name'), machine)
358 oldname = machine.name
361 memory = fields.getfirst('memory')
362 if memory is not None:
363 memory = validation.validMemory(user, memory, machine, on=False)
364 machine.memory = memory
366 disksize = validation.testDisk(user, fields.getfirst('disk'))
367 if disksize is not None:
368 disksize = validation.validDisk(user, disksize, machine)
369 disk = machine.disks[0]
370 if disk.size != disksize:
371 olddisk[disk.guest_device_name] = disksize
373 ctx.current.save(disk)
375 if owner is not None:
376 machine.owner = owner
379 if admin is not None:
380 machine.administrator = admin
381 if contact is not None:
382 machine.contact = contact
384 ctx.current.save(machine)
387 transaction.rollback()
389 for diskname in olddisk:
390 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
392 controls.renameMachine(machine, oldname, name)
393 return dict(user=user,
397 def modify(user, fields):
398 """Handler for modifying attributes of a machine."""
400 modify_dict = modifyDict(user, fields)
401 except InvalidInput, err:
403 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
405 machine = modify_dict['machine']
408 info_dict = infoDict(user, machine)
409 info_dict['err'] = err
411 for field in fields.keys():
412 setattr(info_dict['defaults'], field, fields.getfirst(field))
413 info_dict['result'] = result
414 return templates.info(searchList=[info_dict])
417 def helpHandler(user, fields):
418 """Handler for help messages."""
419 simple = fields.getfirst('simple')
420 subjects = fields.getlist('subject')
422 help_mapping = dict(paravm_console="""
423 ParaVM machines do not support console access over VNC. To access
424 these machines, you either need to boot with a liveCD and ssh in or
425 hope that the sipb-xen maintainers add support for serial consoles.""",
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 see who administers the LOCKER locker using the
438 command '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 quota may have a
446 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
451 subjects = sorted(help_mapping.keys())
456 mapping=help_mapping)
458 return templates.help(searchList=[d])
461 def badOperation(u, e):
462 raise CodeError("Unknown operation")
464 def infoDict(user, machine):
465 status = controls.statusInfo(machine)
466 checkpoint.checkpoint('Getting status info')
467 has_vnc = hasVnc(status)
469 main_status = dict(name=machine.name,
470 memory=str(machine.memory))
474 main_status = dict(status[1:])
475 start_time = float(main_status.get('start_time', 0))
476 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
477 cpu_time_float = float(main_status.get('cpu_time', 0))
478 cputime = datetime.timedelta(seconds=int(cpu_time_float))
479 checkpoint.checkpoint('Status')
480 display_fields = """name uptime memory state cpu_weight on_reboot
481 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
482 display_fields = [('name', 'Name'),
484 ('administrator', 'Administrator'),
485 ('contact', 'Contact'),
488 ('uptime', 'uptime'),
489 ('cputime', 'CPU usage'),
492 ('state', 'state (xen format)'),
493 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
494 ('on_reboot', 'Action on VM reboot'),
495 ('on_poweroff', 'Action on VM poweroff'),
496 ('on_crash', 'Action on VM crash'),
497 ('on_xend_start', 'Action on Xen start'),
498 ('on_xend_stop', 'Action on Xen stop'),
499 ('bootloader', 'Bootloader options'),
503 machine_info['name'] = machine.name
504 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
505 machine_info['owner'] = machine.owner
506 machine_info['administrator'] = machine.administrator
507 machine_info['contact'] = machine.contact
509 nic_fields = getNicInfo(machine_info, machine)
510 nic_point = display_fields.index('NIC_INFO')
511 display_fields = (display_fields[:nic_point] + nic_fields +
512 display_fields[nic_point+1:])
514 disk_fields = getDiskInfo(machine_info, machine)
515 disk_point = display_fields.index('DISK_INFO')
516 display_fields = (display_fields[:disk_point] + disk_fields +
517 display_fields[disk_point+1:])
519 main_status['memory'] += ' MiB'
520 for field, disp in display_fields:
521 if field in ('uptime', 'cputime') and locals()[field] is not None:
522 fields.append((disp, locals()[field]))
523 elif field in machine_info:
524 fields.append((disp, machine_info[field]))
525 elif field in main_status:
526 fields.append((disp, main_status[field]))
529 #fields.append((disp, None))
531 checkpoint.checkpoint('Got fields')
534 max_mem = validation.maxMemory(user, machine, False)
535 checkpoint.checkpoint('Got mem')
536 max_disk = validation.maxDisk(user, machine)
537 defaults = Defaults()
538 for name in 'machine_id name administrator owner memory contact'.split():
539 setattr(defaults, name, getattr(machine, name))
540 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
541 checkpoint.checkpoint('Got defaults')
543 cdroms=CDROM.select(),
544 on=status is not None,
552 owner_help=helppopup("owner"),
556 def info(user, fields):
557 """Handler for info on a single VM."""
558 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
559 d = infoDict(user, machine)
560 checkpoint.checkpoint('Got infodict')
561 return templates.info(searchList=[d])
563 mapping = dict(list=listVms,
571 def printHeaders(headers):
572 for key, value in headers.iteritems():
573 print '%s: %s' % (key, value)
578 """Return the current user based on the SSL environment variables"""
579 if 'SSL_CLIENT_S_DN_Email' in os.environ:
580 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
585 def main(operation, user, fields):
586 start_time = time.time()
587 fun = mapping.get(operation, badOperation)
589 if fun not in (helpHandler, ):
590 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
592 checkpoint.checkpoint('Before')
593 output = fun(u, fields)
594 checkpoint.checkpoint('After')
596 headers = dict(DEFAULT_HEADERS)
597 if isinstance(output, tuple):
598 new_headers, output = output
599 headers.update(new_headers)
600 e = revertStandardError()
603 printHeaders(headers)
604 output_string = str(output)
605 checkpoint.checkpoint('output as a string')
607 print '<pre>%s</pre>' % checkpoint
608 except Exception, err:
609 if not fields.has_key('js'):
610 if isinstance(err, CodeError):
611 print 'Content-Type: text/html\n'
612 e = revertStandardError()
613 print error(operation, u, fields, err, e)
615 if isinstance(err, InvalidInput):
616 print 'Content-Type: text/html\n'
617 e = revertStandardError()
618 print invalidInput(operation, u, fields, err, e)
620 print 'Content-Type: text/plain\n'
621 print 'Uh-oh! We experienced an error.'
622 print 'Please email sipb-xen@mit.edu with the contents of this page.'
624 e = revertStandardError()
629 if __name__ == '__main__':
630 fields = cgi.FieldStorage()
633 operation = os.environ.get('PATH_INFO', '')
635 print "Status: 301 Moved Permanently"
636 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
639 if operation.startswith('/'):
640 operation = operation[1:]
644 #main(operation, u, fields)
646 profile.run('main(operation, u, fields)', 'log-'+operation)