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)
33 sys.stderr = StringIO()
35 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
38 from Cheetah.Template import Template
39 from sipb_xen_database import Machine, CDROM, ctx, connect
41 from webcommon import InvalidInput, CodeError, g
46 self.start_time = time.time()
49 def checkpoint(self, s):
50 self.checkpoints.append((s, time.time()))
53 return ('Timing info:\n%s\n' %
54 '\n'.join(['%s: %s' % (d, t - self.start_time) for
55 (d, t) in self.checkpoints]))
57 checkpoint = Checkpoint()
61 """Return HTML code for a (?) link to a specified help topic"""
62 return ('<span class="helplink"><a href="help?subject=' + subj +
63 '&simple=true" target="_blank" ' +
64 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
66 def makeErrorPre(old, addition):
70 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
72 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
74 Template.helppopup = staticmethod(helppopup)
78 """Class to store a dictionary that will be converted to JSON"""
79 def __init__(self, **kws):
87 return simplejson.dumps(self.data)
89 def addError(self, text):
90 """Add stderr text to be displayed on the website."""
92 makeErrorPre(self.data.get('err'), text)
95 """Class to store default values for fields."""
101 def __init__(self, max_memory=None, max_disk=None, **kws):
102 if max_memory is not None:
103 self.memory = min(self.memory, max_memory)
104 if max_disk is not None:
105 self.max_disk = min(self.disk, max_disk)
107 setattr(self, key, kws[key])
111 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
113 def error(op, user, fields, err, emsg):
114 """Print an error page when a CodeError occurs"""
115 d = dict(op=op, user=user, errorMessage=str(err),
117 return templates.error(searchList=[d])
119 def invalidInput(op, user, fields, err, emsg):
120 """Print an error page when an InvalidInput exception occurs"""
121 d = dict(op=op, user=user, err_field=err.err_field,
122 err_value=str(err.err_value), stderr=emsg,
123 errorMessage=str(err))
124 return templates.invalid(searchList=[d])
127 """Does the machine with a given status list support VNC?"""
131 if l[0] == 'device' and l[1][0] == 'vfb':
133 return 'location' in d
136 def parseCreate(user, fields):
137 name = fields.getfirst('name')
138 if not validation.validMachineName(name):
139 raise InvalidInput('name', name, 'You must provide a machine name.')
142 if Machine.get_by(name=name):
143 raise InvalidInput('name', name,
144 "Name already exists.")
146 owner = validation.testOwner(user, fields.getfirst('owner'))
148 memory = fields.getfirst('memory')
149 memory = validation.validMemory(user, memory, on=True)
151 disk_size = fields.getfirst('disk')
152 disk_size = validation.validDisk(user, disk_size)
154 vm_type = fields.getfirst('vmtype')
155 if vm_type not in ('hvm', 'paravm'):
156 raise CodeError("Invalid vm type '%s'" % vm_type)
157 is_hvm = (vm_type == 'hvm')
159 cdrom = fields.getfirst('cdrom')
160 if cdrom is not None and not CDROM.get(cdrom):
161 raise CodeError("Invalid cdrom type '%s'" % cdrom)
162 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
163 owner=owner, is_hvm=is_hvm, cdrom=cdrom)
165 def create(user, fields):
166 """Handler for create requests."""
168 parsed_fields = parseCreate(user, fields)
169 machine = controls.createVm(**parsed_fields)
170 except InvalidInput, err:
174 g.clear() #Changed global state
175 d = getListDict(user)
178 for field in fields.keys():
179 setattr(d['defaults'], field, fields.getfirst(field))
181 d['new_machine'] = parsed_fields['name']
182 return templates.list(searchList=[d])
185 def getListDict(user):
186 machines = [m for m in Machine.select()
187 if validation.haveAccess(user, m)]
188 checkpoint.checkpoint('Got my machines')
192 checkpoint.checkpoint('Got uptimes')
194 m.uptime = g.uptimes.get(m)
200 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
201 max_memory = validation.maxMemory(user)
202 max_disk = validation.maxDisk(user)
203 checkpoint.checkpoint('Got max mem/disk')
204 defaults = Defaults(max_memory=max_memory,
208 checkpoint.checkpoint('Got defaults')
210 cant_add_vm=validation.cantAddVm(user),
211 max_memory=max_memory,
217 cdroms=CDROM.select())
220 def listVms(user, fields):
221 """Handler for list requests."""
222 checkpoint.checkpoint('Getting list dict')
223 d = getListDict(user)
224 checkpoint.checkpoint('Got list dict')
225 return templates.list(searchList=[d])
227 def vnc(user, fields):
230 Note that due to same-domain restrictions, the applet connects to
231 the webserver, which needs to forward those requests to the xen
232 server. The Xen server runs another proxy that (1) authenticates
233 and (2) finds the correct port for the VM.
235 You might want iptables like:
237 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
238 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
239 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
240 --dport 10003 -j SNAT --to-source 18.187.7.142
241 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
242 --dport 10003 -j ACCEPT
244 Remember to enable iptables!
245 echo 1 > /proc/sys/net/ipv4/ip_forward
247 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
249 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
253 data["machine"] = machine.name
254 data["expires"] = time.time()+(5*60)
255 pickled_data = cPickle.dumps(data)
256 m = hmac.new(TOKEN_KEY, digestmod=sha)
257 m.update(pickled_data)
258 token = {'data': pickled_data, 'digest': m.digest()}
259 token = cPickle.dumps(token)
260 token = base64.urlsafe_b64encode(token)
262 status = controls.statusInfo(machine)
263 has_vnc = hasVnc(status)
269 hostname=os.environ.get('SERVER_NAME', 'localhost'),
271 return templates.vnc(searchList=[d])
273 def getHostname(nic):
274 if nic.hostname and '.' in nic.hostname:
277 return nic.machine.name + '.servers.csail.mit.edu'
282 def getNicInfo(data_dict, machine):
283 """Helper function for info, get data on nics for a machine.
285 Modifies data_dict to include the relevant data, and returns a list
286 of (key, name) pairs to display "name: data_dict[key]" to the user.
288 data_dict['num_nics'] = len(machine.nics)
289 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
290 ('nic%s_mac', 'NIC %s MAC Addr'),
291 ('nic%s_ip', 'NIC %s IP'),
294 for i in range(len(machine.nics)):
295 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
297 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
298 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
299 data_dict['nic%s_ip' % i] = machine.nics[i].ip
300 if len(machine.nics) == 1:
301 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
304 def getDiskInfo(data_dict, machine):
305 """Helper function for info, get data on disks for a machine.
307 Modifies data_dict to include the relevant data, and returns a list
308 of (key, name) pairs to display "name: data_dict[key]" to the user.
310 data_dict['num_disks'] = len(machine.disks)
311 disk_fields_template = [('%s_size', '%s size')]
313 for disk in machine.disks:
314 name = disk.guest_device_name
315 disk_fields.extend([(x % name, y % name) for x, y in
316 disk_fields_template])
317 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
320 def command(user, fields):
321 """Handler for running commands like boot and delete on a VM."""
322 back = fields.getfirst('back')
324 d = controls.commandResult(user, fields)
325 if d['command'] == 'Delete VM':
327 except InvalidInput, err:
330 print >> sys.stderr, err
335 return templates.command(searchList=[d])
337 g.clear() #Changed global state
338 d = getListDict(user)
340 return templates.list(searchList=[d])
342 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
343 d = infoDict(user, machine)
345 return templates.info(searchList=[d])
348 ('back', back, 'Not a known back page.')
350 def modifyDict(user, fields):
352 transaction = ctx.current.create_transaction()
354 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
355 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
356 admin = validation.testAdmin(user, fields.getfirst('administrator'),
358 contact = validation.testContact(user, fields.getfirst('contact'),
360 name = validation.testName(user, fields.getfirst('name'), machine)
361 oldname = machine.name
364 memory = fields.getfirst('memory')
365 if memory is not None:
366 memory = validation.validMemory(user, memory, machine, on=False)
367 machine.memory = memory
369 disksize = validation.testDisk(user, fields.getfirst('disk'))
370 if disksize is not None:
371 disksize = validation.validDisk(user, disksize, machine)
372 disk = machine.disks[0]
373 if disk.size != disksize:
374 olddisk[disk.guest_device_name] = disksize
376 ctx.current.save(disk)
378 if owner is not None:
379 machine.owner = owner
382 if admin is not None:
383 machine.administrator = admin
384 if contact is not None:
385 machine.contact = contact
387 ctx.current.save(machine)
390 transaction.rollback()
392 for diskname in olddisk:
393 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
395 controls.renameMachine(machine, oldname, name)
396 return dict(user=user,
400 def modify(user, fields):
401 """Handler for modifying attributes of a machine."""
403 modify_dict = modifyDict(user, fields)
404 except InvalidInput, err:
406 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
408 machine = modify_dict['machine']
411 info_dict = infoDict(user, machine)
412 info_dict['err'] = err
414 for field in fields.keys():
415 setattr(info_dict['defaults'], field, fields.getfirst(field))
416 info_dict['result'] = result
417 return templates.info(searchList=[info_dict])
420 def helpHandler(user, fields):
421 """Handler for help messages."""
422 simple = fields.getfirst('simple')
423 subjects = fields.getlist('subject')
425 help_mapping = dict(paravm_console="""
426 ParaVM machines do not support console access over VNC. To access
427 these machines, you either need to boot with a liveCD and ssh in or
428 hope that the sipb-xen maintainers add support for serial consoles.""",
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 see who administers the LOCKER locker using the
441 command '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 quota may have a
449 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
454 subjects = sorted(help_mapping.keys())
459 mapping=help_mapping)
461 return templates.help(searchList=[d])
464 def badOperation(u, e):
465 raise CodeError("Unknown operation")
467 def infoDict(user, machine):
468 status = controls.statusInfo(machine)
469 checkpoint.checkpoint('Getting status info')
470 has_vnc = hasVnc(status)
472 main_status = dict(name=machine.name,
473 memory=str(machine.memory))
477 main_status = dict(status[1:])
478 start_time = float(main_status.get('start_time', 0))
479 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
480 cpu_time_float = float(main_status.get('cpu_time', 0))
481 cputime = datetime.timedelta(seconds=int(cpu_time_float))
482 checkpoint.checkpoint('Status')
483 display_fields = """name uptime memory state cpu_weight on_reboot
484 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
485 display_fields = [('name', 'Name'),
487 ('administrator', 'Administrator'),
488 ('contact', 'Contact'),
491 ('uptime', 'uptime'),
492 ('cputime', 'CPU usage'),
495 ('state', 'state (xen format)'),
496 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
497 ('on_reboot', 'Action on VM reboot'),
498 ('on_poweroff', 'Action on VM poweroff'),
499 ('on_crash', 'Action on VM crash'),
500 ('on_xend_start', 'Action on Xen start'),
501 ('on_xend_stop', 'Action on Xen stop'),
502 ('bootloader', 'Bootloader options'),
506 machine_info['name'] = machine.name
507 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
508 machine_info['owner'] = machine.owner
509 machine_info['administrator'] = machine.administrator
510 machine_info['contact'] = machine.contact
512 nic_fields = getNicInfo(machine_info, machine)
513 nic_point = display_fields.index('NIC_INFO')
514 display_fields = (display_fields[:nic_point] + nic_fields +
515 display_fields[nic_point+1:])
517 disk_fields = getDiskInfo(machine_info, machine)
518 disk_point = display_fields.index('DISK_INFO')
519 display_fields = (display_fields[:disk_point] + disk_fields +
520 display_fields[disk_point+1:])
522 main_status['memory'] += ' MiB'
523 for field, disp in display_fields:
524 if field in ('uptime', 'cputime') and locals()[field] is not None:
525 fields.append((disp, locals()[field]))
526 elif field in machine_info:
527 fields.append((disp, machine_info[field]))
528 elif field in main_status:
529 fields.append((disp, main_status[field]))
532 #fields.append((disp, None))
534 checkpoint.checkpoint('Got fields')
537 max_mem = validation.maxMemory(user, machine, False)
538 checkpoint.checkpoint('Got mem')
539 max_disk = validation.maxDisk(user, machine)
540 defaults = Defaults()
541 for name in 'machine_id name administrator owner memory contact'.split():
542 setattr(defaults, name, getattr(machine, name))
543 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
544 checkpoint.checkpoint('Got defaults')
546 cdroms=CDROM.select(),
547 on=status is not None,
555 owner_help=helppopup("owner"),
559 def info(user, fields):
560 """Handler for info on a single VM."""
561 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
562 d = infoDict(user, machine)
563 checkpoint.checkpoint('Got infodict')
564 return templates.info(searchList=[d])
566 mapping = dict(list=listVms,
574 def printHeaders(headers):
575 for key, value in headers.iteritems():
576 print '%s: %s' % (key, value)
581 """Return the current user based on the SSL environment variables"""
582 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 if os.getenv("SIPB_XEN_PROFILE"):
646 profile.run('main(operation, u, fields)', 'log-'+operation)
648 main(operation, u, fields)