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')
38 from Cheetah.Template import Template
39 from sipb_xen_database import Machine, CDROM, ctx, connect
41 from webcommon import InvalidInput, CodeError, g
45 """Return HTML code for a (?) link to a specified help topic"""
46 return ('<span class="helplink"><a href="help?subject=' + subj +
47 '&simple=true" target="_blank" ' +
48 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
51 """User class (sort of useless, I admit)"""
52 def __init__(self, username, email):
53 self.username = username
56 def makeErrorPre(old, addition):
60 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
62 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
64 Template.helppopup = staticmethod(helppopup)
68 """Class to store a dictionary that will be converted to JSON"""
69 def __init__(self, **kws):
77 return simplejson.dumps(self.data)
79 def addError(self, text):
80 """Add stderr text to be displayed on the website."""
82 makeErrorPre(self.data.get('err'), text)
85 """Class to store default values for fields."""
91 def __init__(self, max_memory=None, max_disk=None, **kws):
92 if max_memory is not None:
93 self.memory = min(self.memory, max_memory)
94 if max_disk is not None:
95 self.max_disk = min(self.disk, max_disk)
97 setattr(self, key, kws[key])
101 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
103 def error(op, user, fields, err, emsg):
104 """Print an error page when a CodeError occurs"""
105 d = dict(op=op, user=user, errorMessage=str(err),
107 return Template(file='error.tmpl', searchList=[d])
109 def invalidInput(op, user, fields, err, emsg):
110 """Print an error page when an InvalidInput exception occurs"""
111 d = dict(op=op, user=user, err_field=err.err_field,
112 err_value=str(err.err_value), stderr=emsg,
113 errorMessage=str(err))
114 return Template(file='invalid.tmpl', searchList=[d])
117 """Does the machine with a given status list support VNC?"""
121 if l[0] == 'device' and l[1][0] == 'vfb':
123 return 'location' in d
126 def parseCreate(user, fields):
127 name = fields.getfirst('name')
128 if not validation.validMachineName(name):
129 raise InvalidInput('name', name, 'You must provide a machine name.')
132 if Machine.get_by(name=name):
133 raise InvalidInput('name', name,
134 "Name already exists.")
136 memory = fields.getfirst('memory')
137 memory = validation.validMemory(user, memory, on=True)
139 disk = fields.getfirst('disk')
140 disk = validation.validDisk(user, disk)
142 vm_type = fields.getfirst('vmtype')
143 if vm_type not in ('hvm', 'paravm'):
144 raise CodeError("Invalid vm type '%s'" % vm_type)
145 is_hvm = (vm_type == 'hvm')
147 cdrom = fields.getfirst('cdrom')
148 if cdrom is not None and not CDROM.get(cdrom):
149 raise CodeError("Invalid cdrom type '%s'" % cdrom)
150 return dict(user=user, name=name, memory=memory, disk=disk,
151 is_hvm=is_hvm, cdrom=cdrom)
153 def create(user, fields):
154 """Handler for create requests."""
156 parsed_fields = parseCreate(user, fields)
157 machine = controls.createVm(**parsed_fields)
158 except InvalidInput, err:
162 g.clear() #Changed global state
163 d = getListDict(user)
166 for field in fields.keys():
167 setattr(d['defaults'], field, fields.getfirst(field))
169 d['new_machine'] = parsed_fields['name']
170 return Template(file='list.tmpl', searchList=[d])
173 def getListDict(user):
174 machines = [m for m in Machine.select()
175 if validation.haveAccess(user, m)]
180 m.uptime = g.uptimes.get(m)
186 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
187 max_memory = validation.maxMemory(user)
188 max_disk = validation.maxDisk(user)
189 defaults = Defaults(max_memory=max_memory,
193 cant_add_vm=validation.cantAddVm(user),
194 max_memory=max_memory,
200 cdroms=CDROM.select())
203 def listVms(user, fields):
204 """Handler for list requests."""
205 d = getListDict(user)
206 return Template(file='list.tmpl', searchList=[d])
208 def vnc(user, fields):
211 Note that due to same-domain restrictions, the applet connects to
212 the webserver, which needs to forward those requests to the xen
213 server. The Xen server runs another proxy that (1) authenticates
214 and (2) finds the correct port for the VM.
216 You might want iptables like:
218 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
219 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
220 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
221 --dport 10003 -j SNAT --to-source 18.187.7.142
222 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
223 --dport 10003 -j ACCEPT
225 Remember to enable iptables!
226 echo 1 > /proc/sys/net/ipv4/ip_forward
228 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
230 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
233 data["user"] = user.username
234 data["machine"] = machine.name
235 data["expires"] = time.time()+(5*60)
236 pickled_data = cPickle.dumps(data)
237 m = hmac.new(TOKEN_KEY, digestmod=sha)
238 m.update(pickled_data)
239 token = {'data': pickled_data, 'digest': m.digest()}
240 token = cPickle.dumps(token)
241 token = base64.urlsafe_b64encode(token)
243 status = controls.statusInfo(machine)
244 has_vnc = hasVnc(status)
250 hostname=os.environ.get('SERVER_NAME', 'localhost'),
252 return Template(file='vnc.tmpl', searchList=[d])
254 def getNicInfo(data_dict, machine):
255 """Helper function for info, get data on nics for a machine.
257 Modifies data_dict to include the relevant data, and returns a list
258 of (key, name) pairs to display "name: data_dict[key]" to the user.
260 data_dict['num_nics'] = len(machine.nics)
261 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
262 ('nic%s_mac', 'NIC %s MAC Addr'),
263 ('nic%s_ip', 'NIC %s IP'),
266 for i in range(len(machine.nics)):
267 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
268 data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname +
269 '.servers.csail.mit.edu')
270 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
271 data_dict['nic%s_ip' % i] = machine.nics[i].ip
272 if len(machine.nics) == 1:
273 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
276 def getDiskInfo(data_dict, machine):
277 """Helper function for info, get data on disks for a machine.
279 Modifies data_dict to include the relevant data, and returns a list
280 of (key, name) pairs to display "name: data_dict[key]" to the user.
282 data_dict['num_disks'] = len(machine.disks)
283 disk_fields_template = [('%s_size', '%s size')]
285 for disk in machine.disks:
286 name = disk.guest_device_name
287 disk_fields.extend([(x % name, y % name) for x, y in
288 disk_fields_template])
289 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
292 def command(user, fields):
293 """Handler for running commands like boot and delete on a VM."""
294 back = fields.getfirst('back')
296 d = controls.commandResult(user, fields)
297 if d['command'] == 'Delete VM':
299 except InvalidInput, err:
302 print >> sys.stderr, err
307 return Template(file='command.tmpl', searchList=[d])
309 g.clear() #Changed global state
310 d = getListDict(user)
312 return Template(file='list.tmpl', searchList=[d])
314 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
315 d = infoDict(user, machine)
317 return Template(file='info.tmpl', searchList=[d])
319 raise InvalidInput('back', back, 'Not a known back page.')
321 def modifyDict(user, fields):
323 transaction = ctx.current.create_transaction()
325 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
326 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
327 admin = validation.testAdmin(user, fields.getfirst('administrator'),
329 contact = validation.testContact(user, fields.getfirst('contact'),
331 hostname = validation.testHostname(owner, fields.getfirst('hostname'),
333 name = validation.testName(user, fields.getfirst('name'), machine)
334 oldname = machine.name
337 memory = fields.getfirst('memory')
338 if memory is not None:
339 memory = validation.validMemory(user, memory, machine, on=False)
340 machine.memory = memory
342 disksize = validation.testDisk(user, fields.getfirst('disk'))
343 if disksize is not None:
344 disksize = validation.validDisk(user, disksize, machine)
345 disk = machine.disks[0]
346 if disk.size != disksize:
347 olddisk[disk.guest_device_name] = disksize
349 ctx.current.save(disk)
351 # XXX first NIC gets hostname on change?
352 # Interface doesn't support more.
353 for nic in machine.nics[:1]:
354 nic.hostname = hostname
355 ctx.current.save(nic)
357 if owner is not None:
358 machine.owner = owner
361 if admin is not None:
362 machine.administrator = admin
363 if contact is not None:
364 machine.contact = contact
366 ctx.current.save(machine)
369 transaction.rollback()
371 for diskname in olddisk:
372 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
374 controls.renameMachine(machine, oldname, name)
375 return dict(user=user,
379 def modify(user, fields):
380 """Handler for modifying attributes of a machine."""
382 modify_dict = modifyDict(user, fields)
383 except InvalidInput, err:
385 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
387 machine = modify_dict['machine']
390 info_dict = infoDict(user, machine)
391 info_dict['err'] = err
393 for field in fields.keys():
394 setattr(info_dict['defaults'], field, fields.getfirst(field))
395 info_dict['result'] = result
396 return Template(file='info.tmpl', searchList=[info_dict])
399 def helpHandler(user, fields):
400 """Handler for help messages."""
401 simple = fields.getfirst('simple')
402 subjects = fields.getlist('subject')
404 help_mapping = dict(paravm_console="""
405 ParaVM machines do not support console access over VNC. To access
406 these machines, you either need to boot with a liveCD and ssh in or
407 hope that the sipb-xen maintainers add support for serial consoles.""",
409 HVM machines use the virtualization features of the processor, while
410 ParaVM machines use Xen's emulation of virtualization features. You
411 want an HVM virtualized machine.""",
413 Don't ask us! We're as mystified as you are.""",
415 The owner field is used to determine <a
416 href="help?subject=quotas">quotas</a>. It must be the name of a
417 locker that you are an AFS administrator of. In particular, you or an
418 AFS group you are a member of must have AFS rlidwka bits on the
419 locker. You can check see who administers the LOCKER locker using the
420 command 'fs la /mit/LOCKER' on Athena.) See also <a
421 href="help?subject=administrator">administrator</a>.""",
423 The administrator field determines who can access the console and
424 power on and off the machine. This can be either a user or a moira
427 Quotas are determined on a per-locker basis. Each quota may have a
428 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
433 subjects = sorted(help_mapping.keys())
438 mapping=help_mapping)
440 return Template(file="help.tmpl", searchList=[d])
443 def badOperation(u, e):
444 raise CodeError("Unknown operation")
446 def infoDict(user, machine):
447 status = controls.statusInfo(machine)
448 has_vnc = hasVnc(status)
450 main_status = dict(name=machine.name,
451 memory=str(machine.memory))
455 main_status = dict(status[1:])
456 start_time = float(main_status.get('start_time', 0))
457 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
458 cpu_time_float = float(main_status.get('cpu_time', 0))
459 cputime = datetime.timedelta(seconds=int(cpu_time_float))
460 display_fields = """name uptime memory state cpu_weight on_reboot
461 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
462 display_fields = [('name', 'Name'),
464 ('administrator', 'Administrator'),
465 ('contact', 'Contact'),
468 ('uptime', 'uptime'),
469 ('cputime', 'CPU usage'),
472 ('state', 'state (xen format)'),
473 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
474 ('on_reboot', 'Action on VM reboot'),
475 ('on_poweroff', 'Action on VM poweroff'),
476 ('on_crash', 'Action on VM crash'),
477 ('on_xend_start', 'Action on Xen start'),
478 ('on_xend_stop', 'Action on Xen stop'),
479 ('bootloader', 'Bootloader options'),
483 machine_info['name'] = machine.name
484 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
485 machine_info['owner'] = machine.owner
486 machine_info['administrator'] = machine.administrator
487 machine_info['contact'] = machine.contact
489 nic_fields = getNicInfo(machine_info, machine)
490 nic_point = display_fields.index('NIC_INFO')
491 display_fields = (display_fields[:nic_point] + nic_fields +
492 display_fields[nic_point+1:])
494 disk_fields = getDiskInfo(machine_info, machine)
495 disk_point = display_fields.index('DISK_INFO')
496 display_fields = (display_fields[:disk_point] + disk_fields +
497 display_fields[disk_point+1:])
499 main_status['memory'] += ' MiB'
500 for field, disp in display_fields:
501 if field in ('uptime', 'cputime') and locals()[field] is not None:
502 fields.append((disp, locals()[field]))
503 elif field in machine_info:
504 fields.append((disp, machine_info[field]))
505 elif field in main_status:
506 fields.append((disp, main_status[field]))
509 #fields.append((disp, None))
510 max_mem = validation.maxMemory(user, machine)
511 max_disk = validation.maxDisk(user, machine)
512 defaults = Defaults()
513 for name in 'machine_id name administrator owner memory contact'.split():
514 setattr(defaults, name, getattr(machine, name))
516 defaults.hostname = machine.nics[0].hostname
517 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
519 cdroms=CDROM.select(),
520 on=status is not None,
528 owner_help=helppopup("owner"),
532 def info(user, fields):
533 """Handler for info on a single VM."""
534 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
535 d = infoDict(user, machine)
536 return Template(file='info.tmpl', searchList=[d])
538 mapping = dict(list=listVms,
546 def printHeaders(headers):
547 for key, value in headers.iteritems():
548 print '%s: %s' % (key, value)
553 """Return the current user based on the SSL environment variables"""
554 if 'SSL_CLIENT_S_DN_Email' in os.environ:
555 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
556 return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
558 return User('moo', 'nobody')
560 def main(operation, user, fields):
561 fun = mapping.get(operation, badOperation)
563 if fun not in (helpHandler, ):
564 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
566 output = fun(u, fields)
568 headers = dict(DEFAULT_HEADERS)
569 if isinstance(output, tuple):
570 new_headers, output = output
571 headers.update(new_headers)
573 e = revertStandardError()
576 printHeaders(headers)
578 except Exception, err:
579 if not fields.has_key('js'):
580 if isinstance(err, CodeError):
581 print 'Content-Type: text/html\n'
582 e = revertStandardError()
583 print error(operation, u, fields, err, e)
585 if isinstance(err, InvalidInput):
586 print 'Content-Type: text/html\n'
587 e = revertStandardError()
588 print invalidInput(operation, u, fields, err, e)
590 print 'Content-Type: text/plain\n'
591 print 'Uh-oh! We experienced an error.'
592 print 'Please email sipb-xen@mit.edu with the contents of this page.'
594 e = revertStandardError()
599 if __name__ == '__main__':
600 start_time = time.time()
601 fields = cgi.FieldStorage()
604 operation = os.environ.get('PATH_INFO', '')
606 print "Status: 301 Moved Permanently"
607 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
610 if operation.startswith('/'):
611 operation = operation[1:]
615 main(operation, u, fields)