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. Max 22 chars, alnum plus \'-\' and \'_\'.')
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')
213 def sortkey(machine):
214 return (machine.owner != user, machine.owner, machine.name)
215 machines = sorted(machines, key=sortkey)
217 cant_add_vm=validation.cantAddVm(user),
218 max_memory=max_memory,
224 cdroms=CDROM.select())
227 def listVms(user, fields):
228 """Handler for list requests."""
229 checkpoint.checkpoint('Getting list dict')
230 d = getListDict(user)
231 checkpoint.checkpoint('Got list dict')
232 return templates.list(searchList=[d])
234 def vnc(user, fields):
237 Note that due to same-domain restrictions, the applet connects to
238 the webserver, which needs to forward those requests to the xen
239 server. The Xen server runs another proxy that (1) authenticates
240 and (2) finds the correct port for the VM.
242 You might want iptables like:
244 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
245 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
246 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
247 --dport 10003 -j SNAT --to-source 18.187.7.142
248 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
249 --dport 10003 -j ACCEPT
251 Remember to enable iptables!
252 echo 1 > /proc/sys/net/ipv4/ip_forward
254 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
256 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
260 data["machine"] = machine.name
261 data["expires"] = time.time()+(5*60)
262 pickled_data = cPickle.dumps(data)
263 m = hmac.new(TOKEN_KEY, digestmod=sha)
264 m.update(pickled_data)
265 token = {'data': pickled_data, 'digest': m.digest()}
266 token = cPickle.dumps(token)
267 token = base64.urlsafe_b64encode(token)
269 status = controls.statusInfo(machine)
270 has_vnc = hasVnc(status)
276 hostname=os.environ.get('SERVER_NAME', 'localhost'),
278 return templates.vnc(searchList=[d])
280 def getHostname(nic):
281 if nic.hostname and '.' in nic.hostname:
284 return nic.machine.name + '.servers.csail.mit.edu'
289 def getNicInfo(data_dict, machine):
290 """Helper function for info, get data on nics for a machine.
292 Modifies data_dict to include the relevant data, and returns a list
293 of (key, name) pairs to display "name: data_dict[key]" to the user.
295 data_dict['num_nics'] = len(machine.nics)
296 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
297 ('nic%s_mac', 'NIC %s MAC Addr'),
298 ('nic%s_ip', 'NIC %s IP'),
301 for i in range(len(machine.nics)):
302 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
304 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
305 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
306 data_dict['nic%s_ip' % i] = machine.nics[i].ip
307 if len(machine.nics) == 1:
308 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
311 def getDiskInfo(data_dict, machine):
312 """Helper function for info, get data on disks for a machine.
314 Modifies data_dict to include the relevant data, and returns a list
315 of (key, name) pairs to display "name: data_dict[key]" to the user.
317 data_dict['num_disks'] = len(machine.disks)
318 disk_fields_template = [('%s_size', '%s size')]
320 for disk in machine.disks:
321 name = disk.guest_device_name
322 disk_fields.extend([(x % name, y % name) for x, y in
323 disk_fields_template])
324 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
327 def command(user, fields):
328 """Handler for running commands like boot and delete on a VM."""
329 back = fields.getfirst('back')
331 d = controls.commandResult(user, fields)
332 if d['command'] == 'Delete VM':
334 except InvalidInput, err:
337 #print >> sys.stderr, err
342 return templates.command(searchList=[d])
344 g.clear() #Changed global state
345 d = getListDict(user)
347 return templates.list(searchList=[d])
349 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
350 return ({'Status': '302',
351 'Location': '/info?machine_id=%d' % machine.machine_id},
352 "You shouldn't see this message.")
354 raise InvalidInput('back', back, 'Not a known back page.')
356 def modifyDict(user, fields):
358 transaction = ctx.current.create_transaction()
360 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
361 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
362 admin = validation.testAdmin(user, fields.getfirst('administrator'),
364 contact = validation.testContact(user, fields.getfirst('contact'),
366 name = validation.testName(user, fields.getfirst('name'), machine)
367 oldname = machine.name
370 memory = fields.getfirst('memory')
371 if memory is not None:
372 memory = validation.validMemory(user, memory, machine, on=False)
373 machine.memory = memory
375 disksize = validation.testDisk(user, fields.getfirst('disk'))
376 if disksize is not None:
377 disksize = validation.validDisk(user, disksize, machine)
378 disk = machine.disks[0]
379 if disk.size != disksize:
380 olddisk[disk.guest_device_name] = disksize
382 ctx.current.save(disk)
384 if owner is not None:
385 machine.owner = owner
388 if admin is not None:
389 machine.administrator = admin
390 if contact is not None:
391 machine.contact = contact
393 ctx.current.save(machine)
396 transaction.rollback()
398 for diskname in olddisk:
399 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
401 controls.renameMachine(machine, oldname, name)
402 return dict(user=user,
406 def modify(user, fields):
407 """Handler for modifying attributes of a machine."""
409 modify_dict = modifyDict(user, fields)
410 except InvalidInput, err:
412 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
414 machine = modify_dict['machine']
417 info_dict = infoDict(user, machine)
418 info_dict['err'] = err
420 for field in fields.keys():
421 setattr(info_dict['defaults'], field, fields.getfirst(field))
422 info_dict['result'] = result
423 return templates.info(searchList=[info_dict])
426 def helpHandler(user, fields):
427 """Handler for help messages."""
428 simple = fields.getfirst('simple')
429 subjects = fields.getlist('subject')
431 help_mapping = dict(paravm_console="""
432 ParaVM machines do not support local console access over VNC. To
433 access the serial console of these machines, you can SSH with Kerberos
434 to sipb-xen-console.mit.edu, using the name of the machine as your
437 HVM machines use the virtualization features of the processor, while
438 ParaVM machines use Xen's emulation of virtualization features. You
439 want an HVM virtualized machine.""",
441 Don't ask us! We're as mystified as you are.""",
443 The owner field is used to determine <a
444 href="help?subject=quotas">quotas</a>. It must be the name of a
445 locker that you are an AFS administrator of. In particular, you or an
446 AFS group you are a member of must have AFS rlidwka bits on the
447 locker. You can check who administers the LOCKER locker using the
448 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
449 href="help?subject=administrator">administrator</a>.""",
451 The administrator field determines who can access the console and
452 power on and off the machine. This can be either a user or a moira
455 Quotas are determined on a per-locker basis. Each locker may have a
456 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
459 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
460 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
461 your machine will run just fine, but the applet's display of the
462 console will suffer artifacts.
467 subjects = sorted(help_mapping.keys())
472 mapping=help_mapping)
474 return templates.help(searchList=[d])
477 def badOperation(u, e):
478 raise CodeError("Unknown operation")
480 def infoDict(user, machine):
481 status = controls.statusInfo(machine)
482 checkpoint.checkpoint('Getting status info')
483 has_vnc = hasVnc(status)
485 main_status = dict(name=machine.name,
486 memory=str(machine.memory))
490 main_status = dict(status[1:])
491 start_time = float(main_status.get('start_time', 0))
492 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
493 cpu_time_float = float(main_status.get('cpu_time', 0))
494 cputime = datetime.timedelta(seconds=int(cpu_time_float))
495 checkpoint.checkpoint('Status')
496 display_fields = """name uptime memory state cpu_weight on_reboot
497 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
498 display_fields = [('name', 'Name'),
500 ('administrator', 'Administrator'),
501 ('contact', 'Contact'),
504 ('uptime', 'uptime'),
505 ('cputime', 'CPU usage'),
508 ('state', 'state (xen format)'),
509 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
510 ('on_reboot', 'Action on VM reboot'),
511 ('on_poweroff', 'Action on VM poweroff'),
512 ('on_crash', 'Action on VM crash'),
513 ('on_xend_start', 'Action on Xen start'),
514 ('on_xend_stop', 'Action on Xen stop'),
515 ('bootloader', 'Bootloader options'),
519 machine_info['name'] = machine.name
520 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
521 machine_info['owner'] = machine.owner
522 machine_info['administrator'] = machine.administrator
523 machine_info['contact'] = machine.contact
525 nic_fields = getNicInfo(machine_info, machine)
526 nic_point = display_fields.index('NIC_INFO')
527 display_fields = (display_fields[:nic_point] + nic_fields +
528 display_fields[nic_point+1:])
530 disk_fields = getDiskInfo(machine_info, machine)
531 disk_point = display_fields.index('DISK_INFO')
532 display_fields = (display_fields[:disk_point] + disk_fields +
533 display_fields[disk_point+1:])
535 main_status['memory'] += ' MiB'
536 for field, disp in display_fields:
537 if field in ('uptime', 'cputime') and locals()[field] is not None:
538 fields.append((disp, locals()[field]))
539 elif field in machine_info:
540 fields.append((disp, machine_info[field]))
541 elif field in main_status:
542 fields.append((disp, main_status[field]))
545 #fields.append((disp, None))
547 checkpoint.checkpoint('Got fields')
550 max_mem = validation.maxMemory(user, machine, False)
551 checkpoint.checkpoint('Got mem')
552 max_disk = validation.maxDisk(user, machine)
553 defaults = Defaults()
554 for name in 'machine_id name administrator owner memory contact'.split():
555 setattr(defaults, name, getattr(machine, name))
556 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
557 checkpoint.checkpoint('Got defaults')
559 cdroms=CDROM.select(),
560 on=status is not None,
568 owner_help=helppopup("owner"),
572 def info(user, fields):
573 """Handler for info on a single VM."""
574 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
575 d = infoDict(user, machine)
576 checkpoint.checkpoint('Got infodict')
577 return templates.info(searchList=[d])
579 mapping = dict(list=listVms,
587 def printHeaders(headers):
588 for key, value in headers.iteritems():
589 print '%s: %s' % (key, value)
594 """Return the current user based on the SSL environment variables"""
595 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
598 def main(operation, user, fields):
599 start_time = time.time()
600 fun = mapping.get(operation, badOperation)
602 if fun not in (helpHandler, ):
603 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
605 checkpoint.checkpoint('Before')
606 output = fun(u, fields)
607 checkpoint.checkpoint('After')
609 headers = dict(DEFAULT_HEADERS)
610 if isinstance(output, tuple):
611 new_headers, output = output
612 headers.update(new_headers)
613 e = revertStandardError()
616 printHeaders(headers)
617 output_string = str(output)
618 checkpoint.checkpoint('output as a string')
620 print '<!-- <pre>%s</pre> -->' % checkpoint
621 except Exception, err:
622 if not fields.has_key('js'):
623 if isinstance(err, CodeError):
624 print 'Content-Type: text/html\n'
625 e = revertStandardError()
626 print error(operation, u, fields, err, e)
628 if isinstance(err, InvalidInput):
629 print 'Content-Type: text/html\n'
630 e = revertStandardError()
631 print invalidInput(operation, u, fields, err, e)
633 print 'Content-Type: text/plain\n'
634 print 'Uh-oh! We experienced an error.'
635 print 'Please email sipb-xen@mit.edu with the contents of this page.'
637 e = revertStandardError()
642 if __name__ == '__main__':
643 fields = cgi.FieldStorage()
646 operation = os.environ.get('PATH_INFO', '')
648 print "Status: 301 Moved Permanently"
649 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
652 if operation.startswith('/'):
653 operation = operation[1:]
657 if os.getenv("SIPB_XEN_PROFILE"):
659 profile.run('main(operation, u, fields)', 'log-'+operation)
661 main(operation, u, fields)