2 """Main CGI script for web interface"""
15 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')
39 from Cheetah.Template import Template
40 import sipb_xen_database
41 from sipb_xen_database import Machine, CDROM, ctx, connect, MachineAccess, Type, Autoinstall
44 from webcommon import InvalidInput, CodeError, g
49 self.start_time = time.time()
52 def checkpoint(self, s):
53 self.checkpoints.append((s, time.time()))
56 return ('Timing info:\n%s\n' %
57 '\n'.join(['%s: %s' % (d, t - self.start_time) for
58 (d, t) in self.checkpoints]))
60 checkpoint = Checkpoint()
63 return "'" + string.replace('\\', '\\\\').replace("'", "\\'").replace('\n', '\\n') + "'"
66 """Return HTML code for a (?) link to a specified help topic"""
67 return ('<span class="helplink"><a href="help?' +
68 cgi.escape(urllib.urlencode(dict(subject=subj, simple='true')))
69 +'" target="_blank" ' +
70 'onclick="return helppopup(' + cgi.escape(jquote(subj)) + ')">(?)</a></span>')
72 def makeErrorPre(old, addition):
76 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
78 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
80 Template.sipb_xen_database = sipb_xen_database
81 Template.helppopup = staticmethod(helppopup)
85 """Class to store a dictionary that will be converted to JSON"""
86 def __init__(self, **kws):
94 return simplejson.dumps(self.data)
96 def addError(self, text):
97 """Add stderr text to be displayed on the website."""
99 makeErrorPre(self.data.get('err'), text)
102 """Class to store default values for fields."""
108 def __init__(self, max_memory=None, max_disk=None, **kws):
109 self.type = Type.get('linux-hvm')
110 if max_memory is not None:
111 self.memory = min(self.memory, max_memory)
112 if max_disk is not None:
113 self.max_disk = min(self.disk, max_disk)
115 setattr(self, key, kws[key])
119 DEFAULT_HEADERS = {'Content-Type': 'text/html'}
121 def error(op, user, fields, err, emsg):
122 """Print an error page when a CodeError occurs"""
123 d = dict(op=op, user=user, errorMessage=str(err),
125 return templates.error(searchList=[d])
127 def invalidInput(op, user, fields, err, emsg):
128 """Print an error page when an InvalidInput exception occurs"""
129 d = dict(op=op, user=user, err_field=err.err_field,
130 err_value=str(err.err_value), stderr=emsg,
131 errorMessage=str(err))
132 return templates.invalid(searchList=[d])
135 """Does the machine with a given status list support VNC?"""
139 if l[0] == 'device' and l[1][0] == 'vfb':
141 return 'location' in d
144 def parseCreate(user, fields):
145 name = fields.getfirst('name')
146 if not validation.validMachineName(name):
147 raise InvalidInput('name', name, 'You must provide a machine name. Max 22 chars, alnum plus \'-\' and \'_\'.')
150 if Machine.get_by(name=name):
151 raise InvalidInput('name', name,
152 "Name already exists.")
154 owner = validation.testOwner(user, fields.getfirst('owner'))
156 memory = fields.getfirst('memory')
157 memory = validation.validMemory(owner, memory, on=True)
159 disk_size = fields.getfirst('disk')
160 disk_size = validation.validDisk(owner, disk_size)
162 vm_type = fields.getfirst('vmtype')
163 vm_type = validation.validVmType(vm_type)
165 cdrom = fields.getfirst('cdrom')
166 if cdrom is not None and not CDROM.get(cdrom):
167 raise CodeError("Invalid cdrom type '%s'" % cdrom)
169 clone_from = fields.getfirst('clone_from')
170 if clone_from and clone_from != 'ice3':
171 raise CodeError("Invalid clone image '%s'" % clone_from)
173 return dict(contact=user, name=name, memory=memory, disk_size=disk_size,
174 owner=owner, machine_type=vm_type, cdrom=cdrom, clone_from=clone_from)
176 def create(user, fields):
177 """Handler for create requests."""
179 parsed_fields = parseCreate(user, fields)
180 machine = controls.createVm(**parsed_fields)
181 except InvalidInput, err:
185 g.clear() #Changed global state
186 d = getListDict(user)
189 for field in fields.keys():
190 setattr(d['defaults'], field, fields.getfirst(field))
192 d['new_machine'] = parsed_fields['name']
193 return templates.list(searchList=[d])
196 def getListDict(user):
197 """Gets the list of local variables used by list.tmpl."""
198 machines = g.machines
199 checkpoint.checkpoint('Got my machines')
203 checkpoint.checkpoint('Got uptimes')
205 m.uptime = g.uptimes.get(m)
211 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
212 max_memory = validation.maxMemory(user)
213 max_disk = validation.maxDisk(user)
214 checkpoint.checkpoint('Got max mem/disk')
215 defaults = Defaults(max_memory=max_memory,
219 checkpoint.checkpoint('Got defaults')
220 def sortkey(machine):
221 return (machine.owner != user, machine.owner, machine.name)
222 machines = sorted(machines, key=sortkey)
224 cant_add_vm=validation.cantAddVm(user),
225 max_memory=max_memory,
233 def listVms(user, fields):
234 """Handler for list requests."""
235 checkpoint.checkpoint('Getting list dict')
236 d = getListDict(user)
237 checkpoint.checkpoint('Got list dict')
238 return templates.list(searchList=[d])
240 def vnc(user, fields):
243 Note that due to same-domain restrictions, the applet connects to
244 the webserver, which needs to forward those requests to the xen
245 server. The Xen server runs another proxy that (1) authenticates
246 and (2) finds the correct port for the VM.
248 You might want iptables like:
250 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
251 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
252 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
253 --dport 10003 -j SNAT --to-source 18.187.7.142
254 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
255 --dport 10003 -j ACCEPT
257 Remember to enable iptables!
258 echo 1 > /proc/sys/net/ipv4/ip_forward
260 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
262 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
266 data["machine"] = machine.name
267 data["expires"] = time.time()+(5*60)
268 pickled_data = cPickle.dumps(data)
269 m = hmac.new(TOKEN_KEY, digestmod=sha)
270 m.update(pickled_data)
271 token = {'data': pickled_data, 'digest': m.digest()}
272 token = cPickle.dumps(token)
273 token = base64.urlsafe_b64encode(token)
275 status = controls.statusInfo(machine)
276 has_vnc = hasVnc(status)
282 hostname=os.environ.get('SERVER_NAME', 'localhost'),
284 return templates.vnc(searchList=[d])
286 def getHostname(nic):
287 """Find the hostname associated with a NIC.
289 XXX this should be merged with the similar logic in DNS and DHCP.
291 if nic.hostname and '.' in nic.hostname:
294 return nic.machine.name + '.xvm.mit.edu'
299 def getNicInfo(data_dict, machine):
300 """Helper function for info, get data on nics for a machine.
302 Modifies data_dict to include the relevant data, and returns a list
303 of (key, name) pairs to display "name: data_dict[key]" to the user.
305 data_dict['num_nics'] = len(machine.nics)
306 nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
307 ('nic%s_mac', 'NIC %s MAC Addr'),
308 ('nic%s_ip', 'NIC %s IP'),
311 for i in range(len(machine.nics)):
312 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
314 data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
315 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
316 data_dict['nic%s_ip' % i] = machine.nics[i].ip
317 if len(machine.nics) == 1:
318 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
321 def getDiskInfo(data_dict, machine):
322 """Helper function for info, get data on disks for a machine.
324 Modifies data_dict to include the relevant data, and returns a list
325 of (key, name) pairs to display "name: data_dict[key]" to the user.
327 data_dict['num_disks'] = len(machine.disks)
328 disk_fields_template = [('%s_size', '%s size')]
330 for disk in machine.disks:
331 name = disk.guest_device_name
332 disk_fields.extend([(x % name, y % name) for x, y in
333 disk_fields_template])
334 data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
337 def command(user, fields):
338 """Handler for running commands like boot and delete on a VM."""
339 back = fields.getfirst('back')
341 d = controls.commandResult(user, fields)
342 if d['command'] == 'Delete VM':
344 except InvalidInput, err:
347 #print >> sys.stderr, err
352 return templates.command(searchList=[d])
354 g.clear() #Changed global state
355 d = getListDict(user)
357 return templates.list(searchList=[d])
359 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
360 return ({'Status': '302',
361 'Location': '/info?machine_id=%d' % machine.machine_id},
362 "You shouldn't see this message.")
364 raise InvalidInput('back', back, 'Not a known back page.')
366 def modifyDict(user, fields):
367 """Modify a machine as specified by CGI arguments.
369 Return a list of local variables for modify.tmpl.
372 transaction = ctx.current.create_transaction()
374 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
375 owner = validation.testOwner(user, fields.getfirst('owner'), machine)
376 admin = validation.testAdmin(user, fields.getfirst('administrator'),
378 contact = validation.testContact(user, fields.getfirst('contact'),
380 name = validation.testName(user, fields.getfirst('name'), machine)
381 oldname = machine.name
384 memory = fields.getfirst('memory')
385 if memory is not None:
386 memory = validation.validMemory(user, memory, machine, on=False)
387 machine.memory = memory
389 vm_type = validation.validVmType(fields.getfirst('vmtype'))
390 if vm_type is not None:
391 machine.type = vm_type
393 disksize = validation.testDisk(user, fields.getfirst('disk'))
394 if disksize is not None:
395 disksize = validation.validDisk(user, disksize, machine)
396 disk = machine.disks[0]
397 if disk.size != disksize:
398 olddisk[disk.guest_device_name] = disksize
400 ctx.current.save(disk)
403 if owner is not None and owner != machine.owner:
404 machine.owner = owner
408 if admin is not None and admin != machine.administrator:
409 machine.administrator = admin
411 if contact is not None:
412 machine.contact = contact
414 ctx.current.save(machine)
416 cache_acls.refreshMachine(machine)
419 transaction.rollback()
421 for diskname in olddisk:
422 controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
424 controls.renameMachine(machine, oldname, name)
425 return dict(user=user,
429 def modify(user, fields):
430 """Handler for modifying attributes of a machine."""
432 modify_dict = modifyDict(user, fields)
433 except InvalidInput, err:
435 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
437 machine = modify_dict['machine']
440 info_dict = infoDict(user, machine)
441 info_dict['err'] = err
443 for field in fields.keys():
444 setattr(info_dict['defaults'], field, fields.getfirst(field))
445 info_dict['result'] = result
446 return templates.info(searchList=[info_dict])
449 def helpHandler(user, fields):
450 """Handler for help messages."""
451 simple = fields.getfirst('simple')
452 subjects = fields.getlist('subject')
454 help_mapping = dict(paravm_console="""
455 ParaVM machines do not support local console access over VNC. To
456 access the serial console of these machines, you can SSH with Kerberos
457 to sipb-xen-console.mit.edu, using the name of the machine as your
460 HVM machines use the virtualization features of the processor, while
461 ParaVM machines use Xen's emulation of virtualization features. You
462 want an HVM virtualized machine.""",
464 Don't ask us! We're as mystified as you are.""",
466 The owner field is used to determine <a
467 href="help?subject=quotas">quotas</a>. It must be the name of a
468 locker that you are an AFS administrator of. In particular, you or an
469 AFS group you are a member of must have AFS rlidwka bits on the
470 locker. You can check who administers the LOCKER locker using the
471 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.) See also <a
472 href="help?subject=administrator">administrator</a>.""",
474 The administrator field determines who can access the console and
475 power on and off the machine. This can be either a user or a moira
478 Quotas are determined on a per-locker basis. Each locker may have a
479 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
482 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
483 setting <tt>fb=false</tt> to disable the framebuffer. If you don't,
484 your machine will run just fine, but the applet's display of the
485 console will suffer artifacts.
490 subjects = sorted(help_mapping.keys())
495 mapping=help_mapping)
497 return templates.help(searchList=[d])
500 def badOperation(u, e):
501 """Function called when accessing an unknown URI."""
502 raise CodeError("Unknown operation")
504 def infoDict(user, machine):
505 """Get the variables used by info.tmpl."""
506 status = controls.statusInfo(machine)
507 checkpoint.checkpoint('Getting status info')
508 has_vnc = hasVnc(status)
510 main_status = dict(name=machine.name,
511 memory=str(machine.memory))
515 main_status = dict(status[1:])
516 start_time = float(main_status.get('start_time', 0))
517 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
518 cpu_time_float = float(main_status.get('cpu_time', 0))
519 cputime = datetime.timedelta(seconds=int(cpu_time_float))
520 checkpoint.checkpoint('Status')
521 display_fields = """name uptime memory state cpu_weight on_reboot
522 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
523 display_fields = [('name', 'Name'),
525 ('administrator', 'Administrator'),
526 ('contact', 'Contact'),
529 ('uptime', 'uptime'),
530 ('cputime', 'CPU usage'),
533 ('state', 'state (xen format)'),
534 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
535 ('on_reboot', 'Action on VM reboot'),
536 ('on_poweroff', 'Action on VM poweroff'),
537 ('on_crash', 'Action on VM crash'),
538 ('on_xend_start', 'Action on Xen start'),
539 ('on_xend_stop', 'Action on Xen stop'),
540 ('bootloader', 'Bootloader options'),
544 machine_info['name'] = machine.name
545 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
546 machine_info['owner'] = machine.owner
547 machine_info['administrator'] = machine.administrator
548 machine_info['contact'] = machine.contact
550 nic_fields = getNicInfo(machine_info, machine)
551 nic_point = display_fields.index('NIC_INFO')
552 display_fields = (display_fields[:nic_point] + nic_fields +
553 display_fields[nic_point+1:])
555 disk_fields = getDiskInfo(machine_info, machine)
556 disk_point = display_fields.index('DISK_INFO')
557 display_fields = (display_fields[:disk_point] + disk_fields +
558 display_fields[disk_point+1:])
560 main_status['memory'] += ' MiB'
561 for field, disp in display_fields:
562 if field in ('uptime', 'cputime') and locals()[field] is not None:
563 fields.append((disp, locals()[field]))
564 elif field in machine_info:
565 fields.append((disp, machine_info[field]))
566 elif field in main_status:
567 fields.append((disp, main_status[field]))
570 #fields.append((disp, None))
572 checkpoint.checkpoint('Got fields')
575 max_mem = validation.maxMemory(user, machine, False)
576 checkpoint.checkpoint('Got mem')
577 max_disk = validation.maxDisk(user, machine)
578 defaults = Defaults()
579 for name in 'machine_id name administrator owner memory contact type'.split():
580 setattr(defaults, name, getattr(machine, name))
581 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
582 checkpoint.checkpoint('Got defaults')
584 on=status is not None,
592 owner_help=helppopup("owner"),
596 def info(user, fields):
597 """Handler for info on a single VM."""
598 machine = validation.testMachineId(user, fields.getfirst('machine_id'))
599 d = infoDict(user, machine)
600 checkpoint.checkpoint('Got infodict')
601 return templates.info(searchList=[d])
603 mapping = dict(list=listVms,
611 def printHeaders(headers):
612 """Print a dictionary as HTTP headers."""
613 for key, value in headers.iteritems():
614 print '%s: %s' % (key, value)
619 """Return the current user based on the SSL environment variables"""
620 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
623 def main(operation, user, fields):
624 start_time = time.time()
625 fun = mapping.get(operation, badOperation)
627 if fun not in (helpHandler, ):
628 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
630 checkpoint.checkpoint('Before')
631 output = fun(u, fields)
632 checkpoint.checkpoint('After')
634 headers = dict(DEFAULT_HEADERS)
635 if isinstance(output, tuple):
636 new_headers, output = output
637 headers.update(new_headers)
638 e = revertStandardError()
641 printHeaders(headers)
642 output_string = str(output)
643 checkpoint.checkpoint('output as a string')
645 print '<!-- <pre>%s</pre> -->' % checkpoint
646 except Exception, err:
647 if not fields.has_key('js'):
648 if isinstance(err, CodeError):
649 print 'Content-Type: text/html\n'
650 e = revertStandardError()
651 print error(operation, u, fields, err, e)
653 if isinstance(err, InvalidInput):
654 print 'Content-Type: text/html\n'
655 e = revertStandardError()
656 print invalidInput(operation, u, fields, err, e)
658 print 'Content-Type: text/plain\n'
659 print 'Uh-oh! We experienced an error.'
660 print 'Please email sipb-xen@mit.edu with the contents of this page.'
662 e = revertStandardError()
667 if __name__ == '__main__':
668 fields = cgi.FieldStorage()
671 operation = os.environ.get('PATH_INFO', '')
673 print "Status: 301 Moved Permanently"
674 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
677 if operation.startswith('/'):
678 operation = operation[1:]
682 if os.getenv("SIPB_XEN_PROFILE"):
684 profile.run('main(operation, u, fields)', 'log-'+operation)
686 main(operation, u, fields)