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, MachineAccess
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(owner, memory, on=True)
151 disk_size = fields.getfirst('disk')
152 disk_size = validation.validDisk(owner, 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 = g.machines
187 checkpoint.checkpoint('Got my machines')
191 checkpoint.checkpoint('Got uptimes')
193 m.uptime = g.uptimes.get(m)
199 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
200 max_memory = validation.maxMemory(user)
201 max_disk = validation.maxDisk(user)
202 checkpoint.checkpoint('Got max mem/disk')
203 defaults = Defaults(max_memory=max_memory,
207 checkpoint.checkpoint('Got defaults')
209 cant_add_vm=validation.cantAddVm(user),
210 max_memory=max_memory,
216 cdroms=CDROM.select())
219 def listVms(user, fields):
220 """Handler for list requests."""
221 checkpoint.checkpoint('Getting list dict')
222 d = getListDict(user)
223 checkpoint.checkpoint('Got list dict')
224 return templates.list(searchList=[d])
226 def vnc(user, 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.testMachineId(user, fields.getfirst('machine_id'))
248 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
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)
261 status = controls.statusInfo(machine)
262 has_vnc = hasVnc(status)
268 hostname=os.environ.get('SERVER_NAME', 'localhost'),
270 return templates.vnc(searchList=[d])
272 def getHostname(nic):
273 if nic.hostname and '.' in nic.hostname:
276 return nic.machine.name + '.servers.csail.mit.edu'
281 def getNicInfo(data_dict, machine):
282 """Helper function for info, get data on nics for a machine.
284 Modifies data_dict to include the relevant data, and returns a list
285 of (key, name) pairs to display "name: data_dict[key]" to the user.
287 data_dict['num_nics'] = len(machine.nics)
288 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
289 ('nic%s_mac', 'NIC %s MAC Addr'),
290 ('nic%s_ip', 'NIC %s IP'),
293 for i in range(len(machine.nics)):
294 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
296 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
297 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
298 data_dict['nic%s_ip' % i] = machine.nics[i].ip
299 if len(machine.nics) == 1:
300 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
303 def getDiskInfo(data_dict, machine):
304 """Helper function for info, get data on disks for a machine.
306 Modifies data_dict to include the relevant data, and returns a list
307 of (key, name) pairs to display "name: data_dict[key]" to the user.
309 data_dict['num_disks'] = len(machine.disks)
310 disk_fields_template = [('%s_size', '%s size')]
312 for disk in machine.disks:
313 name = disk.guest_device_name
314 disk_fields.extend([(x % name, y % name) for x, y in
315 disk_fields_template])
316 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
319 def command(user, fields):
320 """Handler for running commands like boot and delete on a VM."""
321 back = fields.getfirst('back')
323 d = controls.commandResult(user, fields)
324 if d['command'] == 'Delete VM':
326 except InvalidInput, err:
329 #print >> sys.stderr, err
334 return templates.command(searchList=[d])
336 g.clear() #Changed global state
337 d = getListDict(user)
339 return templates.list(searchList=[d])
341 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
342 d = infoDict(user, machine)
344 return templates.info(searchList=[d])
346 raise InvalidInput('back', back, 'Not a known back page.')
348 def modifyDict(user, fields):
350 transaction = ctx.current.create_transaction()
352 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
353 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
354 admin = validation.testAdmin(user, fields.getfirst('administrator'),
356 contact = validation.testContact(user, fields.getfirst('contact'),
358 name = validation.testName(user, fields.getfirst('name'), machine)
359 oldname = machine.name
362 memory = fields.getfirst('memory')
363 if memory is not None:
364 memory = validation.validMemory(user, memory, machine, on=False)
365 machine.memory = memory
367 disksize = validation.testDisk(user, fields.getfirst('disk'))
368 if disksize is not None:
369 disksize = validation.validDisk(user, disksize, machine)
370 disk = machine.disks[0]
371 if disk.size != disksize:
372 olddisk[disk.guest_device_name] = disksize
374 ctx.current.save(disk)
376 if owner is not None:
377 machine.owner = owner
380 if admin is not None:
381 machine.administrator = admin
382 if contact is not None:
383 machine.contact = contact
385 ctx.current.save(machine)
388 transaction.rollback()
390 for diskname in olddisk:
391 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
393 controls.renameMachine(machine, oldname, name)
394 return dict(user=user,
398 def modify(user, fields):
399 """Handler for modifying attributes of a machine."""
401 modify_dict = modifyDict(user, fields)
402 except InvalidInput, err:
404 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
406 machine = modify_dict['machine']
409 info_dict = infoDict(user, machine)
410 info_dict['err'] = err
412 for field in fields.keys():
413 setattr(info_dict['defaults'], field, fields.getfirst(field))
414 info_dict['result'] = result
415 return templates.info(searchList=[info_dict])
418 def helpHandler(user, fields):
419 """Handler for help messages."""
420 simple = fields.getfirst('simple')
421 subjects = fields.getlist('subject')
423 help_mapping = dict(paravm_console="""
424 ParaVM machines do not support console access over VNC. To access
425 these machines, you either need to boot with a liveCD and ssh in or
426 hope that the sipb-xen maintainers add support for serial consoles.""",
428 HVM machines use the virtualization features of the processor, while
429 ParaVM machines use Xen's emulation of virtualization features. You
430 want an HVM virtualized machine.""",
432 Don't ask us! We're as mystified as you are.""",
434 The owner field is used to determine <a
435 href="help?subject=quotas">quotas</a>. It must be the name of a
436 locker that you are an AFS administrator of. In particular, you or an
437 AFS group you are a member of must have AFS rlidwka bits on the
438 locker. You can check see who administers the LOCKER locker using the
439 command 'fs la /mit/LOCKER' on Athena.) See also <a
440 href="help?subject=administrator">administrator</a>.""",
442 The administrator field determines who can access the console and
443 power on and off the machine. This can be either a user or a moira
446 Quotas are determined on a per-locker basis. Each quota may have a
447 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
450 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
451 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
452 your machine will run just fine, but the applet's display of the
453 console will suffer artifacts.
458 subjects = sorted(help_mapping.keys())
463 mapping=help_mapping)
465 return templates.help(searchList=[d])
468 def badOperation(u, e):
469 raise CodeError("Unknown operation")
471 def infoDict(user, machine):
472 status = controls.statusInfo(machine)
473 checkpoint.checkpoint('Getting status info')
474 has_vnc = hasVnc(status)
476 main_status = dict(name=machine.name,
477 memory=str(machine.memory))
481 main_status = dict(status[1:])
482 start_time = float(main_status.get('start_time', 0))
483 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
484 cpu_time_float = float(main_status.get('cpu_time', 0))
485 cputime = datetime.timedelta(seconds=int(cpu_time_float))
486 checkpoint.checkpoint('Status')
487 display_fields = """name uptime memory state cpu_weight on_reboot
488 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
489 display_fields = [('name', 'Name'),
491 ('administrator', 'Administrator'),
492 ('contact', 'Contact'),
495 ('uptime', 'uptime'),
496 ('cputime', 'CPU usage'),
499 ('state', 'state (xen format)'),
500 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
501 ('on_reboot', 'Action on VM reboot'),
502 ('on_poweroff', 'Action on VM poweroff'),
503 ('on_crash', 'Action on VM crash'),
504 ('on_xend_start', 'Action on Xen start'),
505 ('on_xend_stop', 'Action on Xen stop'),
506 ('bootloader', 'Bootloader options'),
510 machine_info['name'] = machine.name
511 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
512 machine_info['owner'] = machine.owner
513 machine_info['administrator'] = machine.administrator
514 machine_info['contact'] = machine.contact
516 nic_fields = getNicInfo(machine_info, machine)
517 nic_point = display_fields.index('NIC_INFO')
518 display_fields = (display_fields[:nic_point] + nic_fields +
519 display_fields[nic_point+1:])
521 disk_fields = getDiskInfo(machine_info, machine)
522 disk_point = display_fields.index('DISK_INFO')
523 display_fields = (display_fields[:disk_point] + disk_fields +
524 display_fields[disk_point+1:])
526 main_status['memory'] += ' MiB'
527 for field, disp in display_fields:
528 if field in ('uptime', 'cputime') and locals()[field] is not None:
529 fields.append((disp, locals()[field]))
530 elif field in machine_info:
531 fields.append((disp, machine_info[field]))
532 elif field in main_status:
533 fields.append((disp, main_status[field]))
536 #fields.append((disp, None))
538 checkpoint.checkpoint('Got fields')
541 max_mem = validation.maxMemory(user, machine, False)
542 checkpoint.checkpoint('Got mem')
543 max_disk = validation.maxDisk(user, machine)
544 defaults = Defaults()
545 for name in 'machine_id name administrator owner memory contact'.split():
546 setattr(defaults, name, getattr(machine, name))
547 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
548 checkpoint.checkpoint('Got defaults')
550 cdroms=CDROM.select(),
551 on=status is not None,
559 owner_help=helppopup("owner"),
563 def info(user, fields):
564 """Handler for info on a single VM."""
565 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
566 d = infoDict(user, machine)
567 checkpoint.checkpoint('Got infodict')
568 return templates.info(searchList=[d])
570 mapping = dict(list=listVms,
578 def printHeaders(headers):
579 for key, value in headers.iteritems():
580 print '%s: %s' % (key, value)
585 """Return the current user based on the SSL environment variables"""
586 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
589 def main(operation, user, fields):
590 start_time = time.time()
591 fun = mapping.get(operation, badOperation)
593 if fun not in (helpHandler, ):
594 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
596 checkpoint.checkpoint('Before')
597 output = fun(u, fields)
598 checkpoint.checkpoint('After')
600 headers = dict(DEFAULT_HEADERS)
601 if isinstance(output, tuple):
602 new_headers, output = output
603 headers.update(new_headers)
604 e = revertStandardError()
607 printHeaders(headers)
608 output_string = str(output)
609 checkpoint.checkpoint('output as a string')
611 print '<pre>%s</pre>' % checkpoint
612 except Exception, err:
613 if not fields.has_key('js'):
614 if isinstance(err, CodeError):
615 print 'Content-Type: text/html\n'
616 e = revertStandardError()
617 print error(operation, u, fields, err, e)
619 if isinstance(err, InvalidInput):
620 print 'Content-Type: text/html\n'
621 e = revertStandardError()
622 print invalidInput(operation, u, fields, err, e)
624 print 'Content-Type: text/plain\n'
625 print 'Uh-oh! We experienced an error.'
626 print 'Please email sipb-xen@mit.edu with the contents of this page.'
628 e = revertStandardError()
633 if __name__ == '__main__':
634 fields = cgi.FieldStorage()
637 operation = os.environ.get('PATH_INFO', '')
639 print "Status: 301 Moved Permanently"
640 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
643 if operation.startswith('/'):
644 operation = operation[1:]
648 if os.getenv("SIPB_XEN_PROFILE"):
650 profile.run('main(operation, u, fields)', 'log-'+operation)
652 main(operation, u, fields)