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 vm_type = validation.validVmType(vm_type)
157 cdrom = fields.getfirst('cdrom')
158 if cdrom is not None and not CDROM.get(cdrom):
159 raise CodeError("Invalid cdrom type '%s'" % cdrom)
161 clone_from = fields.getfirst('clone_from')
162 if clone_from and clone_from != 'ice3':
163 raise CodeError("Invalid clone image '%s'" % clone_from)
165 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
166 owner=owner, machine_type=vm_type, cdrom=cdrom, clone_from=clone_from)
168 def create(user, fields):
169 """Handler for create requests."""
171 parsed_fields = parseCreate(user, fields)
172 machine = controls.createVm(**parsed_fields)
173 except InvalidInput, err:
177 g.clear() #Changed global state
178 d = getListDict(user)
181 for field in fields.keys():
182 setattr(d['defaults'], field, fields.getfirst(field))
184 d['new_machine'] = parsed_fields['name']
185 return templates.list(searchList=[d])
188 def getListDict(user):
189 """Gets the list of local variables used by list.tmpl."""
190 machines = g.machines
191 checkpoint.checkpoint('Got my machines')
195 checkpoint.checkpoint('Got uptimes')
197 m.uptime = g.uptimes.get(m)
203 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
204 max_memory = validation.maxMemory(user)
205 max_disk = validation.maxDisk(user)
206 checkpoint.checkpoint('Got max mem/disk')
207 defaults = Defaults(max_memory=max_memory,
211 checkpoint.checkpoint('Got defaults')
212 def sortkey(machine):
213 return (machine.owner != user, machine.owner, machine.name)
214 machines = sorted(machines, key=sortkey)
216 cant_add_vm=validation.cantAddVm(user),
217 max_memory=max_memory,
223 cdroms=CDROM.select())
226 def listVms(user, fields):
227 """Handler for list requests."""
228 checkpoint.checkpoint('Getting list dict')
229 d = getListDict(user)
230 checkpoint.checkpoint('Got list dict')
231 return templates.list(searchList=[d])
233 def vnc(user, fields):
236 Note that due to same-domain restrictions, the applet connects to
237 the webserver, which needs to forward those requests to the xen
238 server. The Xen server runs another proxy that (1) authenticates
239 and (2) finds the correct port for the VM.
241 You might want iptables like:
243 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
244 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
245 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
246 --dport 10003 -j SNAT --to-source 18.187.7.142
247 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
248 --dport 10003 -j ACCEPT
250 Remember to enable iptables!
251 echo 1 > /proc/sys/net/ipv4/ip_forward
253 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
255 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
259 data["machine"] = machine.name
260 data["expires"] = time.time()+(5*60)
261 pickled_data = cPickle.dumps(data)
262 m = hmac.new(TOKEN_KEY, digestmod=sha)
263 m.update(pickled_data)
264 token = {'data': pickled_data, 'digest': m.digest()}
265 token = cPickle.dumps(token)
266 token = base64.urlsafe_b64encode(token)
268 status = controls.statusInfo(machine)
269 has_vnc = hasVnc(status)
275 hostname=os.environ.get('SERVER_NAME', 'localhost'),
277 return templates.vnc(searchList=[d])
279 def getHostname(nic):
280 """Find the hostname associated with a NIC.
282 XXX this should be merged with the similar logic in DNS and DHCP.
284 if nic.hostname and '.' in nic.hostname:
287 return nic.machine.name + '.servers.csail.mit.edu'
292 def getNicInfo(data_dict, machine):
293 """Helper function for info, get data on nics for a machine.
295 Modifies data_dict to include the relevant data, and returns a list
296 of (key, name) pairs to display "name: data_dict[key]" to the user.
298 data_dict['num_nics'] = len(machine.nics)
299 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
300 ('nic%s_mac', 'NIC %s MAC Addr'),
301 ('nic%s_ip', 'NIC %s IP'),
304 for i in range(len(machine.nics)):
305 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
307 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
308 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
309 data_dict['nic%s_ip' % i] = machine.nics[i].ip
310 if len(machine.nics) == 1:
311 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
314 def getDiskInfo(data_dict, machine):
315 """Helper function for info, get data on disks for a machine.
317 Modifies data_dict to include the relevant data, and returns a list
318 of (key, name) pairs to display "name: data_dict[key]" to the user.
320 data_dict['num_disks'] = len(machine.disks)
321 disk_fields_template = [('%s_size', '%s size')]
323 for disk in machine.disks:
324 name = disk.guest_device_name
325 disk_fields.extend([(x % name, y % name) for x, y in
326 disk_fields_template])
327 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
330 def command(user, fields):
331 """Handler for running commands like boot and delete on a VM."""
332 back = fields.getfirst('back')
334 d = controls.commandResult(user, fields)
335 if d['command'] == 'Delete VM':
337 except InvalidInput, err:
340 #print >> sys.stderr, err
345 return templates.command(searchList=[d])
347 g.clear() #Changed global state
348 d = getListDict(user)
350 return templates.list(searchList=[d])
352 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
353 return ({'Status': '302',
354 'Location': '/info?machine_id=%d' % machine.machine_id},
355 "You shouldn't see this message.")
357 raise InvalidInput('back', back, 'Not a known back page.')
359 def modifyDict(user, fields):
360 """Modify a machine as specified by CGI arguments.
362 Return a list of local variables for modify.tmpl.
365 transaction = ctx.current.create_transaction()
367 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
368 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
369 admin = validation.testAdmin(user, fields.getfirst('administrator'),
371 contact = validation.testContact(user, fields.getfirst('contact'),
373 name = validation.testName(user, fields.getfirst('name'), machine)
374 oldname = machine.name
377 memory = fields.getfirst('memory')
378 if memory is not None:
379 memory = validation.validMemory(user, memory, machine, on=False)
380 machine.memory = memory
382 disksize = validation.testDisk(user, fields.getfirst('disk'))
383 if disksize is not None:
384 disksize = validation.validDisk(user, disksize, machine)
385 disk = machine.disks[0]
386 if disk.size != disksize:
387 olddisk[disk.guest_device_name] = disksize
389 ctx.current.save(disk)
391 if owner is not None:
392 machine.owner = owner
395 if admin is not None:
396 machine.administrator = admin
397 if contact is not None:
398 machine.contact = contact
400 ctx.current.save(machine)
403 transaction.rollback()
405 for diskname in olddisk:
406 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
408 controls.renameMachine(machine, oldname, name)
409 return dict(user=user,
413 def modify(user, fields):
414 """Handler for modifying attributes of a machine."""
416 modify_dict = modifyDict(user, fields)
417 except InvalidInput, err:
419 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
421 machine = modify_dict['machine']
424 info_dict = infoDict(user, machine)
425 info_dict['err'] = err
427 for field in fields.keys():
428 setattr(info_dict['defaults'], field, fields.getfirst(field))
429 info_dict['result'] = result
430 return templates.info(searchList=[info_dict])
433 def helpHandler(user, fields):
434 """Handler for help messages."""
435 simple = fields.getfirst('simple')
436 subjects = fields.getlist('subject')
438 help_mapping = dict(paravm_console="""
439 ParaVM machines do not support local console access over VNC. To
440 access the serial console of these machines, you can SSH with Kerberos
441 to sipb-xen-console.mit.edu, using the name of the machine as your
444 HVM machines use the virtualization features of the processor, while
445 ParaVM machines use Xen's emulation of virtualization features. You
446 want an HVM virtualized machine.""",
448 Don't ask us! We're as mystified as you are.""",
450 The owner field is used to determine <a
451 href="help?subject=quotas">quotas</a>. It must be the name of a
452 locker that you are an AFS administrator of. In particular, you or an
453 AFS group you are a member of must have AFS rlidwka bits on the
454 locker. You can check who administers the LOCKER locker using the
455 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
456 href="help?subject=administrator">administrator</a>.""",
458 The administrator field determines who can access the console and
459 power on and off the machine. This can be either a user or a moira
462 Quotas are determined on a per-locker basis. Each locker may have a
463 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
466 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
467 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
468 your machine will run just fine, but the applet's display of the
469 console will suffer artifacts.
474 subjects = sorted(help_mapping.keys())
479 mapping=help_mapping)
481 return templates.help(searchList=[d])
484 def badOperation(u, e):
485 """Function called when accessing an unknown URI."""
486 raise CodeError("Unknown operation")
488 def infoDict(user, machine):
489 """Get the variables used by info.tmpl."""
490 status = controls.statusInfo(machine)
491 checkpoint.checkpoint('Getting status info')
492 has_vnc = hasVnc(status)
494 main_status = dict(name=machine.name,
495 memory=str(machine.memory))
499 main_status = dict(status[1:])
500 start_time = float(main_status.get('start_time', 0))
501 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
502 cpu_time_float = float(main_status.get('cpu_time', 0))
503 cputime = datetime.timedelta(seconds=int(cpu_time_float))
504 checkpoint.checkpoint('Status')
505 display_fields = """name uptime memory state cpu_weight on_reboot
506 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
507 display_fields = [('name', 'Name'),
509 ('administrator', 'Administrator'),
510 ('contact', 'Contact'),
513 ('uptime', 'uptime'),
514 ('cputime', 'CPU usage'),
517 ('state', 'state (xen format)'),
518 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
519 ('on_reboot', 'Action on VM reboot'),
520 ('on_poweroff', 'Action on VM poweroff'),
521 ('on_crash', 'Action on VM crash'),
522 ('on_xend_start', 'Action on Xen start'),
523 ('on_xend_stop', 'Action on Xen stop'),
524 ('bootloader', 'Bootloader options'),
528 machine_info['name'] = machine.name
529 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
530 machine_info['owner'] = machine.owner
531 machine_info['administrator'] = machine.administrator
532 machine_info['contact'] = machine.contact
534 nic_fields = getNicInfo(machine_info, machine)
535 nic_point = display_fields.index('NIC_INFO')
536 display_fields = (display_fields[:nic_point] + nic_fields +
537 display_fields[nic_point+1:])
539 disk_fields = getDiskInfo(machine_info, machine)
540 disk_point = display_fields.index('DISK_INFO')
541 display_fields = (display_fields[:disk_point] + disk_fields +
542 display_fields[disk_point+1:])
544 main_status['memory'] += ' MiB'
545 for field, disp in display_fields:
546 if field in ('uptime', 'cputime') and locals()[field] is not None:
547 fields.append((disp, locals()[field]))
548 elif field in machine_info:
549 fields.append((disp, machine_info[field]))
550 elif field in main_status:
551 fields.append((disp, main_status[field]))
554 #fields.append((disp, None))
556 checkpoint.checkpoint('Got fields')
559 max_mem = validation.maxMemory(user, machine, False)
560 checkpoint.checkpoint('Got mem')
561 max_disk = validation.maxDisk(user, machine)
562 defaults = Defaults()
563 for name in 'machine_id name administrator owner memory contact'.split():
564 setattr(defaults, name, getattr(machine, name))
565 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
566 checkpoint.checkpoint('Got defaults')
568 cdroms=CDROM.select(),
569 on=status is not None,
577 owner_help=helppopup("owner"),
581 def info(user, fields):
582 """Handler for info on a single VM."""
583 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
584 d = infoDict(user, machine)
585 checkpoint.checkpoint('Got infodict')
586 return templates.info(searchList=[d])
588 mapping = dict(list=listVms,
596 def printHeaders(headers):
597 """Print a dictionary as HTTP headers."""
598 for key, value in headers.iteritems():
599 print '%s: %s' % (key, value)
604 """Return the current user based on the SSL environment variables"""
605 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
608 def main(operation, user, fields):
609 start_time = time.time()
610 fun = mapping.get(operation, badOperation)
612 if fun not in (helpHandler, ):
613 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
615 checkpoint.checkpoint('Before')
616 output = fun(u, fields)
617 checkpoint.checkpoint('After')
619 headers = dict(DEFAULT_HEADERS)
620 if isinstance(output, tuple):
621 new_headers, output = output
622 headers.update(new_headers)
623 e = revertStandardError()
626 printHeaders(headers)
627 output_string = str(output)
628 checkpoint.checkpoint('output as a string')
630 print '<!-- <pre>%s</pre> -->' % checkpoint
631 except Exception, err:
632 if not fields.has_key('js'):
633 if isinstance(err, CodeError):
634 print 'Content-Type: text/html\n'
635 e = revertStandardError()
636 print error(operation, u, fields, err, e)
638 if isinstance(err, InvalidInput):
639 print 'Content-Type: text/html\n'
640 e = revertStandardError()
641 print invalidInput(operation, u, fields, err, e)
643 print 'Content-Type: text/plain\n'
644 print 'Uh-oh! We experienced an error.'
645 print 'Please email sipb-xen@mit.edu with the contents of this page.'
647 e = revertStandardError()
652 if __name__ == '__main__':
653 fields = cgi.FieldStorage()
656 operation = os.environ.get('PATH_INFO', '')
658 print "Status: 301 Moved Permanently"
659 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
662 if operation.startswith('/'):
663 operation = operation[1:]
667 if os.getenv("SIPB_XEN_PROFILE"):
669 profile.run('main(operation, u, fields)', 'log-'+operation)
671 main(operation, u, fields)