2 """Main CGI script for web interface"""
19 from StringIO import StringIO
22 def revertStandardError():
23 """Move stderr to stdout, and return the contents of the old stderr."""
25 if not isinstance(errio, StringIO):
27 sys.stderr = sys.stdout
32 """Revert stderr to stdout, and print the contents of stderr"""
33 if isinstance(sys.stderr, StringIO):
34 print revertStandardError()
36 if __name__ == '__main__':
38 atexit.register(printError)
39 sys.stderr = StringIO()
41 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
43 from Cheetah.Template import Template
44 from sipb_xen_database import *
46 class MyException(Exception):
47 """Base class for my exceptions"""
50 class InvalidInput(MyException):
51 """Exception for user-provided input is invalid but maybe in good faith.
53 This would include setting memory to negative (which might be a
54 typo) but not setting an invalid boot CD (which requires bypassing
57 def __init__(self, err_field, err_value, expl=None):
58 MyException.__init__(self, expl)
59 self.err_field = err_field
60 self.err_value = err_value
62 class CodeError(MyException):
63 """Exception for internal errors or bad faith input."""
67 """Return HTML code for a (?) link to a specified help topic"""
68 return ('<span class="helplink"><a href="help?subject=' + subj +
69 '&simple=true" target="_blank" ' +
70 'onclick="return helppopup(\'' + subj + '\')">(?)</a></span>')
73 """Global state of the system, to avoid duplicate remctls to get state"""
74 def __init__(self, user):
77 def __get_uptimes(self):
78 if not hasattr(self, '_uptimes'):
79 self._uptimes = getUptimes(Machine.select())
81 uptimes = property(__get_uptimes)
84 """Clear the state so future accesses reload it."""
85 for attr in ('_uptimes', ):
86 if hasattr(self, attr):
92 """User class (sort of useless, I admit)"""
93 def __init__(self, username, email):
94 self.username = username
97 def makeErrorPre(old, addition):
101 return old[:-6] + '\n----\n' + str(addition) + '</pre>'
103 return '<p>STDERR:</p><pre>' + str(addition) + '</pre>'
105 Template.helppopup = staticmethod(helppopup)
109 """Class to store a dictionary that will be converted to JSON"""
110 def __init__(self, **kws):
118 return simplejson.dumps(self.data)
120 def addError(self, text):
121 """Add stderr text to be displayed on the website."""
123 makeErrorPre(self.data.get('err'), text)
126 """Class to store default values for fields."""
132 def __init__(self, max_memory=None, max_disk=None, **kws):
133 if max_memory is not None:
134 self.memory = min(self.memory, max_memory)
135 if max_disk is not None:
136 self.max_disk = min(self.disk, max_disk)
138 setattr(self, key, kws[key])
142 default_headers = {'Content-Type': 'text/html'}
144 # ... and stolen from xend/uuid.py
146 """Generate a random UUID."""
148 return [ random.randint(0, 255) for _ in range(0, 16) ]
151 """Turn a numeric UUID to a hyphen-seperated one."""
152 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
153 "%02x" * 6]) % tuple(u)
155 MAX_MEMORY_TOTAL = 512
156 MAX_MEMORY_SINGLE = 256
157 MIN_MEMORY_SINGLE = 16
160 MIN_DISK_SINGLE = 0.1
164 def getMachinesByOwner(user, machine=None):
165 """Return the machines owned by the same as a machine.
167 If the machine is None, return the machines owned by the same
171 owner = machine.owner
173 owner = user.username
174 return Machine.select_by(owner=owner)
176 def maxMemory(user, machine=None, on=True):
177 """Return the maximum memory for a machine or a user.
179 If machine is None, return the memory available for a new
180 machine. Else, return the maximum that machine can have.
182 on is whether the machine should be turned on. If false, the max
183 memory for the machine to change to, if it is left off, is
187 return MAX_MEMORY_SINGLE
188 machines = getMachinesByOwner(user, machine)
189 active_machines = [x for x in machines if g.uptimes[x]]
190 mem_usage = sum([x.memory for x in active_machines if x != machine])
191 return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
193 def maxDisk(user, machine=None):
194 machines = getMachinesByOwner(user, machine)
195 disk_usage = sum([sum([y.size for y in x.disks])
196 for x in machines if x != machine])
197 return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
200 machines = getMachinesByOwner(user)
201 active_machines = [x for x in machines if g.uptimes[x]]
202 if len(machines) >= MAX_VMS_TOTAL:
203 return 'You have too many VMs to create a new one.'
204 if len(active_machines) >= MAX_VMS_ACTIVE:
205 return ('You already have the maximum number of VMs turned on. '
206 'To create more, turn one off.')
209 def haveAccess(user, machine):
210 """Return whether a user has adminstrative access to a machine"""
211 if user.username == 'moo':
213 if user.username in (machine.administrator, machine.owner):
215 if getafsgroups.checkAfsGroup(user.username, machine.administrator,
216 'athena.mit.edu'): #XXX Cell?
218 if getafsgroups.checkLockerOwner(user.username, machine.owner):
220 return owns(user, machine)
222 def owns(user, machine):
223 """Return whether a user owns a machine"""
224 if user.username == 'moo':
226 return getafsgroups.checkLockerOwner(user.username, machine.owner)
228 def error(op, user, fields, err, emsg):
229 """Print an error page when a CodeError occurs"""
230 d = dict(op=op, user=user, errorMessage=str(err),
232 return Template(file='error.tmpl', searchList=[d]);
234 def invalidInput(op, user, fields, err, emsg):
235 """Print an error page when an InvalidInput exception occurs"""
236 d = dict(op=op, user=user, err_field=err.err_field,
237 err_value=str(err.err_value), stderr=emsg,
238 errorMessage=str(err))
239 return Template(file='invalid.tmpl', searchList=[d]);
241 def validMachineName(name):
242 """Check that name is valid for a machine name"""
245 charset = string.ascii_letters + string.digits + '-_'
246 if name[0] in '-_' or len(name) > 22:
253 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
254 """Kinit with a given username and keytab"""
256 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
257 stderr=subprocess.PIPE)
260 raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
263 """If we lack tickets, kinit."""
264 p = subprocess.Popen(['klist', '-s'])
268 def remctl(*args, **kws):
269 """Perform a remctl and return the output.
271 kinits if necessary, and outputs errors to stderr.
274 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
276 stdout=subprocess.PIPE,
277 stderr=subprocess.PIPE)
280 return p.stdout.read(), p.stderr.read()
282 print >> sys.stderr, 'Error', v, 'on remctl', args, ':'
283 print >> sys.stderr, p.stderr.read()
284 raise CodeError('ERROR on remctl')
285 return p.stdout.read()
287 def lvcreate(machine, disk):
288 """Create a single disk for a machine"""
289 remctl('web', 'lvcreate', machine.name,
290 disk.guest_device_name, str(disk.size))
292 def makeDisks(machine):
293 """Update the lvm partitions to add a disk."""
294 for disk in machine.disks:
295 lvcreate(machine, disk)
297 def bootMachine(machine, cdtype):
298 """Boot a machine with a given boot CD.
300 If cdtype is None, give no boot cd. Otherwise, it is the string
301 id of the CD (e.g. 'gutsy_i386')
303 if cdtype is not None:
304 remctl('control', machine.name, 'create',
307 remctl('control', machine.name, 'create')
309 def registerMachine(machine):
310 """Register a machine to be controlled by the web interface"""
311 remctl('web', 'register', machine.name)
313 def unregisterMachine(machine):
314 """Unregister a machine to not be controlled by the web interface"""
315 remctl('web', 'unregister', machine.name)
318 """Parse a status string into nested tuples of strings.
320 s = output of xm list --long <machine_name>
322 values = re.split('([()])', s)
324 for v in values[2:-2]: #remove initial and final '()'
331 if len(stack[-1]) == 1:
333 stack[-2].append(stack[-1])
338 stack[-1].extend(v.split())
341 def getUptimes(machines=None):
342 """Return a dictionary mapping machine names to uptime strings"""
343 value_string = remctl('web', 'listvms')
344 lines = value_string.splitlines()
349 uptime = ' '.join(lst[2:])
353 ans[m] = d.get(m.name)
356 def statusInfo(machine):
357 """Return the status list for a given machine.
359 Gets and parses xm list --long
361 value_string, err_string = remctl('control', machine.name, 'list-long',
363 if 'Unknown command' in err_string:
364 raise CodeError("ERROR in remctl list-long %s is not registered" %
366 elif 'does not exist' in err_string:
369 raise CodeError("ERROR in remctl list-long %s: %s" %
370 (machine.name, err_string))
371 status = parseStatus(value_string)
375 """Does the machine with a given status list support VNC?"""
379 if l[0] == 'device' and l[1][0] == 'vfb':
381 return 'location' in d
384 def createVm(user, name, memory, disk, is_hvm, cdrom):
385 """Create a VM and put it in the database"""
386 # put stuff in the table
387 transaction = ctx.current.create_transaction()
389 if memory > maxMemory(user):
390 raise InvalidInput('memory', memory,
391 "Max %s" % maxMemory(user))
392 if disk > maxDisk(user) * 1024:
393 raise InvalidInput('disk', disk,
394 "Max %s" % maxDisk(user))
395 reason = cantAddVm(user)
397 raise InvalidInput('create', True, reason)
398 res = meta.engine.execute('select nextval('
399 '\'"machines_machine_id_seq"\')')
400 id = res.fetchone()[0]
402 machine.machine_id = id
404 machine.memory = memory
405 machine.owner = user.username
406 machine.administrator = user.username
407 machine.contact = user.email
408 machine.uuid = uuidToString(randomUUID())
409 machine.boot_off_cd = True
410 machine_type = Type.get_by(hvm=is_hvm)
411 machine.type_id = machine_type.type_id
412 ctx.current.save(machine)
413 disk = Disk(machine.machine_id,
415 open_nics = NIC.select_by(machine_id=None)
416 if not open_nics: #No IPs left!
417 raise CodeError("No IP addresses left! "
418 "Contact sipb-xen-dev@mit.edu")
420 nic.machine_id = machine.machine_id
422 ctx.current.save(nic)
423 ctx.current.save(disk)
426 transaction.rollback()
428 registerMachine(machine)
430 # tell it to boot with cdrom
431 bootMachine(machine, cdrom)
435 def validMemory(user, memory, machine=None, on=True):
436 """Parse and validate limits for memory for a given user and machine.
438 on is whether the memory must be valid after the machine is
443 if memory < MIN_MEMORY_SINGLE:
446 raise InvalidInput('memory', memory,
447 "Minimum %s MB" % MIN_MEMORY_SINGLE)
448 if memory > maxMemory(user, machine, on):
449 raise InvalidInput('memory', memory,
450 'Maximum %s MB' % maxMemory(user, machine))
453 def validDisk(user, disk, machine=None):
454 """Parse and validate limits for disk for a given user and machine."""
457 if disk > maxDisk(user, machine):
458 raise InvalidInput('disk', disk,
459 "Maximum %s G" % maxDisk(user, machine))
460 disk = int(disk * 1024)
461 if disk < MIN_DISK_SINGLE * 1024:
464 raise InvalidInput('disk', disk,
465 "Minimum %s GB" % MIN_DISK_SINGLE)
468 def parseCreate(user, fields):
469 name = fields.getfirst('name')
470 if not validMachineName(name):
471 raise InvalidInput('name', name, 'You must provide a machine name.')
474 if Machine.get_by(name=name):
475 raise InvalidInput('name', name,
476 "Name already exists.")
478 memory = fields.getfirst('memory')
479 memory = validMemory(user, memory, on=True)
481 disk = fields.getfirst('disk')
482 disk = validDisk(user, disk)
484 vm_type = fields.getfirst('vmtype')
485 if vm_type not in ('hvm', 'paravm'):
486 raise CodeError("Invalid vm type '%s'" % vm_type)
487 is_hvm = (vm_type == 'hvm')
489 cdrom = fields.getfirst('cdrom')
490 if cdrom is not None and not CDROM.get(cdrom):
491 raise CodeError("Invalid cdrom type '%s'" % cdrom)
492 return dict(user=user, name=name, memory=memory, disk=disk,
493 is_hvm=is_hvm, cdrom=cdrom)
495 def create(user, fields):
496 """Handler for create requests."""
497 js = fields.getfirst('js')
499 parsed_fields = parseCreate(user, fields)
500 machine = createVm(**parsed_fields)
501 except InvalidInput, err:
509 return Template(file='create.tmpl', searchList=[d])
510 g.clear() #Changed global state
511 d = getListDict(user)
514 for field in fields.keys():
515 setattr(d['defaults'], field, fields.getfirst(field))
517 d['new_machine'] = parsed_fields['name']
518 t = Template(file='list.tmpl', searchList=[d])
519 return JsonDict(createtable=t.createTable(),
520 machinelist=t.machineList(d['machines']))
523 def getListDict(user):
524 machines = [m for m in Machine.select() if haveAccess(user, m)]
529 m.uptime = g.uptimes.get(m)
535 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
537 # status = statusInfo(m)
538 # on[m.name] = status is not None
539 # has_vnc[m.name] = hasVnc(status)
540 max_memory = maxMemory(user)
541 max_disk = maxDisk(user)
542 defaults = Defaults(max_memory=max_memory,
546 cant_add_vm=cantAddVm(user),
547 max_memory=max_memory,
553 cdroms=CDROM.select())
556 def listVms(user, fields):
557 """Handler for list requests."""
558 d = getListDict(user)
559 t = Template(file='list.tmpl', searchList=[d])
560 js = fields.getfirst('js')
563 if js == 'machinelist':
564 return t.machineList(d['machines'])
565 elif js.startswith('machinerow-'):
566 request_machine_id = int(js.split('-')[1])
567 m = [x for x in d['machines'] if x.id == request_machine_id]
568 return t.machineRow(m)
569 elif js == 'createtable':
570 return t.createTable()
572 def testMachineId(user, machineId, exists=True):
573 """Parse, validate and check authorization for a given machineId.
575 If exists is False, don't check that it exists.
577 if machineId is None:
578 raise CodeError("No machine ID specified")
580 machineId = int(machineId)
582 raise CodeError("Invalid machine ID '%s'" % machineId)
583 machine = Machine.get(machineId)
584 if exists and machine is None:
585 raise CodeError("No such machine ID '%s'" % machineId)
586 if machine is not None and not haveAccess(user, machine):
587 raise CodeError("No access to machine ID '%s'" % machineId)
590 def vnc(user, fields):
593 Note that due to same-domain restrictions, the applet connects to
594 the webserver, which needs to forward those requests to the xen
595 server. The Xen server runs another proxy that (1) authenticates
596 and (2) finds the correct port for the VM.
598 You might want iptables like:
600 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
601 --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
602 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
603 --dport 10003 -j SNAT --to-source 18.187.7.142
604 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
605 --dport 10003 -j ACCEPT
607 Remember to enable iptables!
608 echo 1 > /proc/sys/net/ipv4/ip_forward
610 machine = testMachineId(user, fields.getfirst('machine_id'))
612 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
615 data["user"] = user.username
616 data["machine"] = machine.name
617 data["expires"] = time.time()+(5*60)
618 pickled_data = cPickle.dumps(data)
619 m = hmac.new(TOKEN_KEY, digestmod=sha)
620 m.update(pickled_data)
621 token = {'data': pickled_data, 'digest': m.digest()}
622 token = cPickle.dumps(token)
623 token = base64.urlsafe_b64encode(token)
625 status = statusInfo(machine)
626 has_vnc = hasVnc(status)
632 hostname=os.environ.get('SERVER_NAME', 'localhost'),
634 return Template(file='vnc.tmpl', searchList=[d])
636 def getNicInfo(data_dict, machine):
637 """Helper function for info, get data on nics for a machine.
639 Modifies data_dict to include the relevant data, and returns a list
640 of (key, name) pairs to display "name: data_dict[key]" to the user.
642 data_dict['num_nics'] = len(machine.nics)
643 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
644 ('nic%s_mac', 'NIC %s MAC Addr'),
645 ('nic%s_ip', 'NIC %s IP'),
648 for i in range(len(machine.nics)):
649 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
650 data_dict['nic%s_hostname' % i] = (machine.nics[i].hostname +
651 '.servers.csail.mit.edu')
652 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
653 data_dict['nic%s_ip' % i] = machine.nics[i].ip
654 if len(machine.nics) == 1:
655 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
658 def getDiskInfo(data_dict, machine):
659 """Helper function for info, get data on disks for a machine.
661 Modifies data_dict to include the relevant data, and returns a list
662 of (key, name) pairs to display "name: data_dict[key]" to the user.
664 data_dict['num_disks'] = len(machine.disks)
665 disk_fields_template = [('%s_size', '%s size')]
667 for disk in machine.disks:
668 name = disk.guest_device_name
669 disk_fields.extend([(x % name, y % name) for x, y in
670 disk_fields_template])
671 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
674 def deleteVM(machine):
676 remctl('control', machine.name, 'destroy', err=True)
677 transaction = ctx.current.create_transaction()
678 delete_disk_pairs = [(machine.name, d.guest_device_name)
679 for d in machine.disks]
681 for nic in machine.nics:
682 nic.machine_id = None
684 ctx.current.save(nic)
685 for disk in machine.disks:
686 ctx.current.delete(disk)
687 ctx.current.delete(machine)
690 transaction.rollback()
692 for mname, dname in delete_disk_pairs:
693 remctl('web', 'lvremove', mname, dname)
694 unregisterMachine(machine)
696 def commandResult(user, fields):
697 print >> sys.stderr, time.time()-start_time
698 machine = testMachineId(user, fields.getfirst('machine_id'))
699 action = fields.getfirst('action')
700 cdrom = fields.getfirst('cdrom')
701 print >> sys.stderr, time.time()-start_time
702 if cdrom is not None and not CDROM.get(cdrom):
703 raise CodeError("Invalid cdrom type '%s'" % cdrom)
704 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown',
706 raise CodeError("Invalid action '%s'" % action)
707 if action == 'Reboot':
708 if cdrom is not None:
709 out, err = remctl('control', machine.name, 'reboot', cdrom,
712 out, err = remctl('control', machine.name, 'reboot',
715 if re.match("Error: Domain '.*' does not exist.", err):
716 raise InvalidInput("action", "reboot",
719 print >> sys.stderr, 'Error on reboot:'
720 print >> sys.stderr, err
721 raise CodeError('ERROR on remctl')
723 elif action == 'Power on':
724 if maxMemory(user) < machine.memory:
725 raise InvalidInput('action', 'Power on',
726 "You don't have enough free RAM quota "
727 "to turn on this machine.")
728 bootMachine(machine, cdrom)
729 elif action == 'Power off':
730 out, err = remctl('control', machine.name, 'destroy', err=True)
732 if re.match("Error: Domain '.*' does not exist.", err):
733 raise InvalidInput("action", "Power off",
734 "Machine is not on.")
736 print >> sys.stderr, 'Error on power off:'
737 print >> sys.stderr, err
738 raise CodeError('ERROR on remctl')
739 elif action == 'Shutdown':
740 out, err = remctl('control', machine.name, 'shutdown', err=True)
742 if re.match("Error: Domain '.*' does not exist.", err):
743 raise InvalidInput("action", "Shutdown",
744 "Machine is not on.")
746 print >> sys.stderr, 'Error on Shutdown:'
747 print >> sys.stderr, err
748 raise CodeError('ERROR on remctl')
749 elif action == 'Delete VM':
751 print >> sys.stderr, time.time()-start_time
758 def command(user, fields):
759 """Handler for running commands like boot and delete on a VM."""
760 js = fields.getfirst('js')
762 d = commandResult(user, fields)
763 except InvalidInput, err:
771 return Template(file='command.tmpl', searchList=[d])
773 g.clear() #Changed global state
774 d = getListDict(user)
775 t = Template(file='list.tmpl', searchList=[d])
776 return JsonDict(createtable=t.createTable(),
777 machinelist=t.machineList(d['machines']),
781 machine = testMachineId(user, fields.getfirst('machine_id'))
782 d = infoDict(user, machine)
783 t = Template(file='info.tmpl', searchList=[d])
784 return JsonDict(info=t.infoTable(),
785 commands=t.commands(),
786 modify=t.modifyForm(),
790 raise InvalidInput('js', js, 'Not a known js type.')
792 def testAdmin(user, admin, machine):
793 if admin in (None, machine.administrator):
795 if admin == user.username:
797 if getafsgroups.checkAfsGroup(user.username, admin, 'athena.mit.edu'):
799 if getafsgroups.checkAfsGroup(user.username, 'system:'+admin,
801 return 'system:'+admin
802 raise InvalidInput('administrator', admin,
803 'You must control the group you move it to.')
805 def testOwner(user, owner, machine):
806 if owner in (None, machine.owner):
808 value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
811 raise InvalidInput('owner', owner, value)
813 def testContact(user, contact, machine=None):
814 if contact in (None, machine.contact):
816 if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
817 raise InvalidInput('contact', contact, "Not a valid email.")
820 def testDisk(user, disksize, machine=None):
823 def testName(user, name, machine=None):
824 if name in (None, machine.name):
826 if not Machine.select_by(name=name):
828 raise InvalidInput('name', name, "Name is already taken.")
830 def testHostname(user, hostname, machine):
831 for nic in machine.nics:
832 if hostname == nic.hostname:
834 # check if doesn't already exist
835 if NIC.select_by(hostname=hostname):
836 raise InvalidInput('hostname', hostname,
838 if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
839 raise InvalidInput('hostname', hostname, "Not a valid hostname; "
840 "must only use number, letters, and dashes.")
843 def modifyDict(user, fields):
845 transaction = ctx.current.create_transaction()
847 machine = testMachineId(user, fields.getfirst('machine_id'))
848 owner = testOwner(user, fields.getfirst('owner'), machine)
849 admin = testAdmin(user, fields.getfirst('administrator'), machine)
850 contact = testContact(user, fields.getfirst('contact'), machine)
851 hostname = testHostname(owner, fields.getfirst('hostname'), machine)
852 name = testName(user, fields.getfirst('name'), machine)
853 oldname = machine.name
856 memory = fields.getfirst('memory')
857 if memory is not None:
858 memory = validMemory(user, memory, machine, on=False)
859 machine.memory = memory
861 disksize = testDisk(user, fields.getfirst('disk'))
862 if disksize is not None:
863 disksize = validDisk(user, disksize, machine)
864 disk = machine.disks[0]
865 if disk.size != disksize:
866 olddisk[disk.guest_device_name] = disksize
868 ctx.current.save(disk)
870 # XXX first NIC gets hostname on change?
871 # Interface doesn't support more.
872 for nic in machine.nics[:1]:
873 nic.hostname = hostname
874 ctx.current.save(nic)
876 if owner is not None:
877 machine.owner = owner
880 if admin is not None:
881 machine.administrator = admin
882 if contact is not None:
883 machine.contact = contact
885 ctx.current.save(machine)
888 transaction.rollback()
890 for diskname in olddisk:
891 remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
893 for disk in machine.disks:
894 remctl("web", "lvrename", oldname, disk.guest_device_name, name)
895 remctl("web", "moveregister", oldname, name)
896 return dict(user=user,
900 def modify(user, fields):
901 """Handler for modifying attributes of a machine."""
902 js = fields.getfirst('js')
904 modify_dict = modifyDict(user, fields)
905 except InvalidInput, err:
909 machine = testMachineId(user, fields.getfirst('machine_id'))
911 machine = modify_dict['machine']
915 return Template(file='command.tmpl', searchList=[modify_dict])
916 info_dict = infoDict(user, machine)
917 info_dict['err'] = err
919 for field in fields.keys():
920 setattr(info_dict['defaults'], field, fields.getfirst(field))
921 t = Template(file='info.tmpl', searchList=[info_dict])
922 return JsonDict(info=t.infoTable(),
923 commands=t.commands(),
924 modify=t.modifyForm(),
929 def helpHandler(user, fields):
930 """Handler for help messages."""
931 simple = fields.getfirst('simple')
932 subjects = fields.getlist('subject')
934 help_mapping = dict(paravm_console="""
935 ParaVM machines do not support console access over VNC. To access
936 these machines, you either need to boot with a liveCD and ssh in or
937 hope that the sipb-xen maintainers add support for serial consoles.""",
939 HVM machines use the virtualization features of the processor, while
940 ParaVM machines use Xen's emulation of virtualization features. You
941 want an HVM virtualized machine.""",
943 Don't ask us! We're as mystified as you are.""",
945 The owner field is used to determine <a
946 href="help?subject=quotas">quotas</a>. It must be the name of a
947 locker that you are an AFS administrator of. In particular, you or an
948 AFS group you are a member of must have AFS rlidwka bits on the
949 locker. You can check see who administers the LOCKER locker using the
950 command 'fs la /mit/LOCKER' on Athena.) See also <a
951 href="help?subject=administrator">administrator</a>.""",
953 The administrator field determines who can access the console and
954 power on and off the machine. This can be either a user or a moira
957 Quotas are determined on a per-locker basis. Each quota may have a
958 maximum of 512 megabytes of active ram, 50 gigabytes of disk, and 4
963 subjects = sorted(help_mapping.keys())
968 mapping=help_mapping)
970 return Template(file="help.tmpl", searchList=[d])
973 def badOperation(u, e):
974 raise CodeError("Unknown operation")
976 def infoDict(user, machine):
977 status = statusInfo(machine)
978 has_vnc = hasVnc(status)
980 main_status = dict(name=machine.name,
981 memory=str(machine.memory))
985 main_status = dict(status[1:])
986 start_time = float(main_status.get('start_time', 0))
987 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
988 cpu_time_float = float(main_status.get('cpu_time', 0))
989 cputime = datetime.timedelta(seconds=int(cpu_time_float))
990 display_fields = """name uptime memory state cpu_weight on_reboot
991 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
992 display_fields = [('name', 'Name'),
994 ('administrator', 'Administrator'),
995 ('contact', 'Contact'),
998 ('uptime', 'uptime'),
999 ('cputime', 'CPU usage'),
1002 ('state', 'state (xen format)'),
1003 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
1004 ('on_reboot', 'Action on VM reboot'),
1005 ('on_poweroff', 'Action on VM poweroff'),
1006 ('on_crash', 'Action on VM crash'),
1007 ('on_xend_start', 'Action on Xen start'),
1008 ('on_xend_stop', 'Action on Xen stop'),
1009 ('bootloader', 'Bootloader options'),
1013 machine_info['name'] = machine.name
1014 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
1015 machine_info['owner'] = machine.owner
1016 machine_info['administrator'] = machine.administrator
1017 machine_info['contact'] = machine.contact
1019 nic_fields = getNicInfo(machine_info, machine)
1020 nic_point = display_fields.index('NIC_INFO')
1021 display_fields = (display_fields[:nic_point] + nic_fields +
1022 display_fields[nic_point+1:])
1024 disk_fields = getDiskInfo(machine_info, machine)
1025 disk_point = display_fields.index('DISK_INFO')
1026 display_fields = (display_fields[:disk_point] + disk_fields +
1027 display_fields[disk_point+1:])
1029 main_status['memory'] += ' MB'
1030 for field, disp in display_fields:
1031 if field in ('uptime', 'cputime') and locals()[field] is not None:
1032 fields.append((disp, locals()[field]))
1033 elif field in machine_info:
1034 fields.append((disp, machine_info[field]))
1035 elif field in main_status:
1036 fields.append((disp, main_status[field]))
1039 #fields.append((disp, None))
1040 max_mem = maxMemory(user, machine)
1041 max_disk = maxDisk(user, machine)
1043 for name in 'machine_id name administrator owner memory contact'.split():
1044 setattr(defaults, name, getattr(machine, name))
1046 defaults.hostname = machine.nics[0].hostname
1047 defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
1049 cdroms=CDROM.select(),
1050 on=status is not None,
1058 owner_help=helppopup("owner"),
1062 def info(user, fields):
1063 """Handler for info on a single VM."""
1064 machine = testMachineId(user, fields.getfirst('machine_id'))
1065 d = infoDict(user, machine)
1066 return Template(file='info.tmpl', searchList=[d])
1068 mapping = dict(list=listVms,
1076 def printHeaders(headers):
1077 for key, value in headers.iteritems():
1078 print '%s: %s' % (key, value)
1083 """Return the current user based on the SSL environment variables"""
1084 if 'SSL_CLIENT_S_DN_Email' in os.environ:
1085 username = os.environ['SSL_CLIENT_S_DN_Email'].split("@")[0]
1086 return User(username, os.environ['SSL_CLIENT_S_DN_Email'])
1088 return User('moo', 'nobody')
1090 if __name__ == '__main__':
1091 start_time = time.time()
1092 fields = cgi.FieldStorage()
1095 operation = os.environ.get('PATH_INFO', '')
1097 print "Status: 301 Moved Permanently"
1098 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
1101 if operation.startswith('/'):
1102 operation = operation[1:]
1108 fun = mapping.get(operation, badOperation)
1110 if fun not in (helpHandler, ):
1111 connect('postgres://sipb-xen@sipb-xen-dev.mit.edu/sipb_xen')
1113 output = fun(u, fields)
1115 headers = dict(default_headers)
1116 if isinstance(output, tuple):
1117 new_headers, output = output
1118 headers.update(new_headers)
1120 e = revertStandardError()
1123 printHeaders(headers)
1125 except Exception, err:
1126 if not fields.has_key('js'):
1127 if isinstance(err, CodeError):
1128 print 'Content-Type: text/html\n'
1129 e = revertStandardError()
1130 print error(operation, u, fields, err, e)
1132 if isinstance(err, InvalidInput):
1133 print 'Content-Type: text/html\n'
1134 e = revertStandardError()
1135 print invalidInput(operation, u, fields, err, e)
1137 print 'Content-Type: text/plain\n'
1138 print 'Uh-oh! We experienced an error.'
1139 print 'Please email sipb-xen@mit.edu with the contents of this page.'
1141 e = revertStandardError()