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)
163 clone_from = fields.getfirst('clone_from')
164 if clone_from and clone_from != 'ice3':
165 raise CodeError("Invalid clone image '%s'" % clone_from)
167 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
168 owner=owner, is_hvm=is_hvm, cdrom=cdrom, clone_from=clone_from)
170 def create(user, fields):
171 """Handler for create requests."""
173 parsed_fields = parseCreate(user, fields)
174 machine = controls.createVm(**parsed_fields)
175 except InvalidInput, err:
179 g.clear() #Changed global state
180 d = getListDict(user)
183 for field in fields.keys():
184 setattr(d['defaults'], field, fields.getfirst(field))
186 d['new_machine'] = parsed_fields['name']
187 return templates.list(searchList=[d])
190 def getListDict(user):
191 machines = g.machines
192 checkpoint.checkpoint('Got my machines')
196 checkpoint.checkpoint('Got uptimes')
198 m.uptime = g.uptimes.get(m)
204 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
205 max_memory = validation.maxMemory(user)
206 max_disk = validation.maxDisk(user)
207 checkpoint.checkpoint('Got max mem/disk')
208 defaults = Defaults(max_memory=max_memory,
212 checkpoint.checkpoint('Got defaults')
214 cant_add_vm=validation.cantAddVm(user),
215 max_memory=max_memory,
221 cdroms=CDROM.select())
224 def listVms(user, fields):
225 """Handler for list requests."""
226 checkpoint.checkpoint('Getting list dict')
227 d = getListDict(user)
228 checkpoint.checkpoint('Got list dict')
229 return templates.list(searchList=[d])
231 def vnc(user, fields):
234 Note that due to same-domain restrictions, the applet connects to
235 the webserver, which needs to forward those requests to the xen
236 server. The Xen server runs another proxy that (1) authenticates
237 and (2) finds the correct port for the VM.
239 You might want iptables like:
241 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
242 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
243 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
244 --dport 10003 -j SNAT --to-source 18.187.7.142
245 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
246 --dport 10003 -j ACCEPT
248 Remember to enable iptables!
249 echo 1 > /proc/sys/net/ipv4/ip_forward
251 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
253 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
257 data["machine"] = machine.name
258 data["expires"] = time.time()+(5*60)
259 pickled_data = cPickle.dumps(data)
260 m = hmac.new(TOKEN_KEY, digestmod=sha)
261 m.update(pickled_data)
262 token = {'data': pickled_data, 'digest': m.digest()}
263 token = cPickle.dumps(token)
264 token = base64.urlsafe_b64encode(token)
266 status = controls.statusInfo(machine)
267 has_vnc = hasVnc(status)
273 hostname=os.environ.get('SERVER_NAME', 'localhost'),
275 return templates.vnc(searchList=[d])
277 def getHostname(nic):
278 if nic.hostname and '.' in nic.hostname:
281 return nic.machine.name + '.servers.csail.mit.edu'
286 def getNicInfo(data_dict, machine):
287 """Helper function for info, get data on nics for a machine.
289 Modifies data_dict to include the relevant data, and returns a list
290 of (key, name) pairs to display "name: data_dict[key]" to the user.
292 data_dict['num_nics'] = len(machine.nics)
293 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
294 ('nic%s_mac', 'NIC %s MAC Addr'),
295 ('nic%s_ip', 'NIC %s IP'),
298 for i in range(len(machine.nics)):
299 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
301 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
302 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
303 data_dict['nic%s_ip' % i] = machine.nics[i].ip
304 if len(machine.nics) == 1:
305 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
308 def getDiskInfo(data_dict, machine):
309 """Helper function for info, get data on disks for a machine.
311 Modifies data_dict to include the relevant data, and returns a list
312 of (key, name) pairs to display "name: data_dict[key]" to the user.
314 data_dict['num_disks'] = len(machine.disks)
315 disk_fields_template = [('%s_size', '%s size')]
317 for disk in machine.disks:
318 name = disk.guest_device_name
319 disk_fields.extend([(x % name, y % name) for x, y in
320 disk_fields_template])
321 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
324 def command(user, fields):
325 """Handler for running commands like boot and delete on a VM."""
326 back = fields.getfirst('back')
328 d = controls.commandResult(user, fields)
329 if d['command'] == 'Delete VM':
331 except InvalidInput, err:
334 #print >> sys.stderr, err
339 return templates.command(searchList=[d])
341 g.clear() #Changed global state
342 d = getListDict(user)
344 return templates.list(searchList=[d])
346 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
347 return ({'Status': '302',
348 'Location': '/info?machine_id=%d' % machine.machine_id},
349 "You shouldn't see this message.")
351 raise InvalidInput('back', back, 'Not a known back page.')
353 def modifyDict(user, fields):
355 transaction = ctx.current.create_transaction()
357 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
358 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
359 admin = validation.testAdmin(user, fields.getfirst('administrator'),
361 contact = validation.testContact(user, fields.getfirst('contact'),
363 name = validation.testName(user, fields.getfirst('name'), machine)
364 oldname = machine.name
367 memory = fields.getfirst('memory')
368 if memory is not None:
369 memory = validation.validMemory(user, memory, machine, on=False)
370 machine.memory = memory
372 disksize = validation.testDisk(user, fields.getfirst('disk'))
373 if disksize is not None:
374 disksize = validation.validDisk(user, disksize, machine)
375 disk = machine.disks[0]
376 if disk.size != disksize:
377 olddisk[disk.guest_device_name] = disksize
379 ctx.current.save(disk)
381 if owner is not None:
382 machine.owner = owner
385 if admin is not None:
386 machine.administrator = admin
387 if contact is not None:
388 machine.contact = contact
390 ctx.current.save(machine)
393 transaction.rollback()
395 for diskname in olddisk:
396 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
398 controls.renameMachine(machine, oldname, name)
399 return dict(user=user,
403 def modify(user, fields):
404 """Handler for modifying attributes of a machine."""
406 modify_dict = modifyDict(user, fields)
407 except InvalidInput, err:
409 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
411 machine = modify_dict['machine']
414 info_dict = infoDict(user, machine)
415 info_dict['err'] = err
417 for field in fields.keys():
418 setattr(info_dict['defaults'], field, fields.getfirst(field))
419 info_dict['result'] = result
420 return templates.info(searchList=[info_dict])
423 def helpHandler(user, fields):
424 """Handler for help messages."""
425 simple = fields.getfirst('simple')
426 subjects = fields.getlist('subject')
428 help_mapping = dict(paravm_console="""
429 ParaVM machines do not support console access over VNC. To access
430 these machines, you either need to boot with a liveCD and ssh in or
431 hope that the sipb-xen maintainers add support for serial consoles.""",
433 HVM machines use the virtualization features of the processor, while
434 ParaVM machines use Xen's emulation of virtualization features. You
435 want an HVM virtualized machine.""",
437 Don't ask us! We're as mystified as you are.""",
439 The owner field is used to determine <a
440 href="help?subject=quotas">quotas</a>. It must be the name of a
441 locker that you are an AFS administrator of. In particular, you or an
442 AFS group you are a member of must have AFS rlidwka bits on the
443 locker. You can check see who administers the LOCKER locker using the
444 command 'fs la /mit/LOCKER' on Athena.) See also <a
445 href="help?subject=administrator">administrator</a>.""",
447 The administrator field determines who can access the console and
448 power on and off the machine. This can be either a user or a moira
451 Quotas are determined on a per-locker basis. Each quota may have a
452 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
455 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
456 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
457 your machine will run just fine, but the applet's display of the
458 console will suffer artifacts.
463 subjects = sorted(help_mapping.keys())
468 mapping=help_mapping)
470 return templates.help(searchList=[d])
473 def badOperation(u, e):
474 raise CodeError("Unknown operation")
476 def infoDict(user, machine):
477 status = controls.statusInfo(machine)
478 checkpoint.checkpoint('Getting status info')
479 has_vnc = hasVnc(status)
481 main_status = dict(name=machine.name,
482 memory=str(machine.memory))
486 main_status = dict(status[1:])
487 start_time = float(main_status.get('start_time', 0))
488 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
489 cpu_time_float = float(main_status.get('cpu_time', 0))
490 cputime = datetime.timedelta(seconds=int(cpu_time_float))
491 checkpoint.checkpoint('Status')
492 display_fields = """name uptime memory state cpu_weight on_reboot
493 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
494 display_fields = [('name', 'Name'),
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['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
517 machine_info['owner'] = machine.owner
518 machine_info['administrator'] = machine.administrator
519 machine_info['contact'] = machine.contact
521 nic_fields = getNicInfo(machine_info, machine)
522 nic_point = display_fields.index('NIC_INFO')
523 display_fields = (display_fields[:nic_point] + nic_fields +
524 display_fields[nic_point+1:])
526 disk_fields = getDiskInfo(machine_info, machine)
527 disk_point = display_fields.index('DISK_INFO')
528 display_fields = (display_fields[:disk_point] + disk_fields +
529 display_fields[disk_point+1:])
531 main_status['memory'] += ' MiB'
532 for field, disp in display_fields:
533 if field in ('uptime', 'cputime') and locals()[field] is not None:
534 fields.append((disp, locals()[field]))
535 elif field in machine_info:
536 fields.append((disp, machine_info[field]))
537 elif field in main_status:
538 fields.append((disp, main_status[field]))
541 #fields.append((disp, None))
543 checkpoint.checkpoint('Got fields')
546 max_mem = validation.maxMemory(user, machine, False)
547 checkpoint.checkpoint('Got mem')
548 max_disk = validation.maxDisk(user, machine)
549 defaults = Defaults()
550 for name in 'machine_id name administrator owner memory contact'.split():
551 setattr(defaults, name, getattr(machine, name))
552 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
553 checkpoint.checkpoint('Got defaults')
555 cdroms=CDROM.select(),
556 on=status is not None,
564 owner_help=helppopup("owner"),
568 def info(user, fields):
569 """Handler for info on a single VM."""
570 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
571 d = infoDict(user, machine)
572 checkpoint.checkpoint('Got infodict')
573 return templates.info(searchList=[d])
575 mapping = dict(list=listVms,
583 def printHeaders(headers):
584 for key, value in headers.iteritems():
585 print '%s: %s' % (key, value)
590 """Return the current user based on the SSL environment variables"""
591 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
594 def main(operation, user, fields):
595 start_time = time.time()
596 fun = mapping.get(operation, badOperation)
598 if fun not in (helpHandler, ):
599 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
601 checkpoint.checkpoint('Before')
602 output = fun(u, fields)
603 checkpoint.checkpoint('After')
605 headers = dict(DEFAULT_HEADERS)
606 if isinstance(output, tuple):
607 new_headers, output = output
608 headers.update(new_headers)
609 e = revertStandardError()
612 printHeaders(headers)
613 output_string = str(output)
614 checkpoint.checkpoint('output as a string')
616 print '<pre>%s</pre>' % checkpoint
617 except Exception, err:
618 if not fields.has_key('js'):
619 if isinstance(err, CodeError):
620 print 'Content-Type: text/html\n'
621 e = revertStandardError()
622 print error(operation, u, fields, err, e)
624 if isinstance(err, InvalidInput):
625 print 'Content-Type: text/html\n'
626 e = revertStandardError()
627 print invalidInput(operation, u, fields, err, e)
629 print 'Content-Type: text/plain\n'
630 print 'Uh-oh! We experienced an error.'
631 print 'Please email sipb-xen@mit.edu with the contents of this page.'
633 e = revertStandardError()
638 if __name__ == '__main__':
639 fields = cgi.FieldStorage()
642 operation = os.environ.get('PATH_INFO', '')
644 print "Status: 301 Moved Permanently"
645 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
648 if operation.startswith('/'):
649 operation = operation[1:]
653 if os.getenv("SIPB_XEN_PROFILE"):
655 profile.run('main(operation, u, fields)', 'log-'+operation)
657 main(operation, u, fields)