16 sys.stderr = sys.stdout
17 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
19 from Cheetah.Template import Template
20 from sipb_xen_database import *
23 class MyException(Exception):
24 """Base class for my exceptions"""
27 class InvalidInput(MyException):
28 """Exception for user-provided input is invalid but maybe in good faith.
30 This would include setting memory to negative (which might be a
31 typo) but not setting an invalid boot CD (which requires bypassing
36 class CodeError(MyException):
37 """Exception for internal errors or bad faith input."""
43 """Return HTML code for a (?) link to a specified help topic"""
44 return '<span class="helplink"><a href="help?subject='+subj+'&simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
48 global_dict['helppopup'] = helppopup
51 # ... and stolen from xend/uuid.py
53 """Generate a random UUID."""
55 return [ random.randint(0, 255) for _ in range(0, 16) ]
58 """Turn a numeric UUID to a hyphen-seperated one."""
59 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
60 "%02x" * 6]) % tuple(u)
62 MAX_MEMORY_TOTAL = 512
63 MAX_MEMORY_SINGLE = 256
64 MIN_MEMORY_SINGLE = 16
71 def getMachinesByOwner(owner):
72 """Return the machines owned by a given owner."""
73 return Machine.select_by(owner=owner)
75 def maxMemory(user, machine=None, on=None):
76 """Return the maximum memory for a machine or a user.
78 If machine is None, return the memory available for a new
79 machine. Else, return the maximum that machine can have.
81 on is a dictionary from machines to booleans, whether a machine is
82 on. If None, it is recomputed. XXX make this global?
85 machines = getMachinesByOwner(user.username)
87 on = getUptimes(machines)
88 active_machines = [x for x in machines if on[x]]
89 mem_usage = sum([x.memory for x in active_machines if x != machine])
90 return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
92 def maxDisk(user, machine=None):
93 machines = getMachinesByOwner(user.username)
94 disk_usage = sum([sum([y.size for y in x.disks])
95 for x in machines if x != machine])
96 return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
98 def canAddVm(user, on=None):
99 machines = getMachinesByOwner(user.username)
101 on = getUptimes(machines)
102 active_machines = [x for x in machines if on[x]]
103 return (len(machines) < MAX_VMS_TOTAL and
104 len(active_machines) < MAX_VMS_ACTIVE)
106 def haveAccess(user, machine):
107 """Return whether a user has access to a machine"""
108 if user.username == 'moo':
110 return machine.owner == user.username
112 def error(op, user, fields, err):
113 """Print an error page when a CodeError occurs"""
114 d = dict(op=op, user=user, errorMessage=str(err))
115 print Template(file='error.tmpl', searchList=[d, global_dict]);
117 def validMachineName(name):
118 """Check that name is valid for a machine name"""
121 charset = string.ascii_letters + string.digits + '-_'
122 if name[0] in '-_' or len(name) > 22:
129 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
130 """Kinit with a given username and keytab"""
132 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
133 stderr=subprocess.PIPE)
136 raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
139 """If we lack tickets, kinit."""
140 p = subprocess.Popen(['klist', '-s'])
144 def remctl(*args, **kws):
145 """Perform a remctl and return the output.
147 kinits if necessary, and outputs errors to stderr.
150 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
152 stdout=subprocess.PIPE,
153 stderr=subprocess.PIPE)
156 return p.stdout.read(), p.stderr.read()
158 raise CodeError('ERROR on remctl %s: %s' %
159 (args, p.stderr.read()))
160 return p.stdout.read()
163 """Update the lvm partitions to include all disks in the database."""
164 remctl('web', 'lvcreate')
166 def bootMachine(machine, cdtype):
167 """Boot a machine with a given boot CD.
169 If cdtype is None, give no boot cd. Otherwise, it is the string
170 id of the CD (e.g. 'gutsy_i386')
172 if cdtype is not None:
173 remctl('web', 'vmboot', machine.name,
176 remctl('web', 'vmboot', machine.name)
178 def registerMachine(machine):
179 """Register a machine to be controlled by the web interface"""
180 remctl('web', 'register', machine.name)
182 def unregisterMachine(machine):
183 """Unregister a machine to not be controlled by the web interface"""
184 remctl('web', 'unregister', machine.name)
187 """Parse a status string into nested tuples of strings.
189 s = output of xm list --long <machine_name>
191 values = re.split('([()])', s)
193 for v in values[2:-2]: #remove initial and final '()'
200 if len(stack[-1]) == 1:
202 stack[-2].append(stack[-1])
207 stack[-1].extend(v.split())
210 def getUptimes(machines):
211 """Return a dictionary mapping machine names to uptime strings"""
212 value_string = remctl('web', 'listvms')
213 lines = value_string.splitlines()
218 uptime = ' '.join(lst[2:])
222 ans[m] = d.get(m.name)
225 def statusInfo(machine):
226 """Return the status list for a given machine.
228 Gets and parses xm list --long
230 value_string, err_string = remctl('list-long', machine.name, err=True)
231 if 'Unknown command' in err_string:
232 raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
233 elif 'does not exist' in err_string:
236 raise CodeError("ERROR in remctl list-long %s: %s" % (machine.name, err_string))
237 status = parseStatus(value_string)
241 """Does the machine with a given status list support VNC?"""
245 if l[0] == 'device' and l[1][0] == 'vfb':
247 return 'location' in d
250 def createVm(user, name, memory, disk, is_hvm, cdrom):
251 """Create a VM and put it in the database"""
252 # put stuff in the table
253 transaction = ctx.current.create_transaction()
255 if memory > maxMemory(user):
256 raise InvalidInput("Too much memory requested")
257 if disk > maxDisk(user) * 1024:
258 raise InvalidInput("Too much disk requested")
259 if not canAddVm(user):
260 raise InvalidInput("Too many VMs requested")
261 res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
262 id = res.fetchone()[0]
264 machine.machine_id = id
266 machine.memory = memory
267 machine.owner = user.username
268 machine.contact = user.email
269 machine.uuid = uuidToString(randomUUID())
270 machine.boot_off_cd = True
271 machine_type = Type.get_by(hvm=is_hvm)
272 machine.type_id = machine_type.type_id
273 ctx.current.save(machine)
274 disk = Disk(machine.machine_id,
276 open = NIC.select_by(machine_id=None)
277 if not open: #No IPs left!
278 raise CodeError("No IP addresses left! Contact sipb-xen-dev@mit.edu")
280 nic.machine_id = machine.machine_id
282 ctx.current.save(nic)
283 ctx.current.save(disk)
286 transaction.rollback()
288 registerMachine(machine)
290 # tell it to boot with cdrom
291 bootMachine(machine, cdrom)
295 def validMemory(user, memory, machine=None):
296 """Parse and validate limits for memory for a given user and machine."""
299 if memory < MIN_MEMORY_SINGLE:
302 raise InvalidInput("Invalid memory amount; must be at least %s MB" %
304 if memory > maxMemory(user, machine):
305 raise InvalidInput("Too much memory requested")
308 def validDisk(user, disk, machine=None):
309 """Parse and validate limits for disk for a given user and machine."""
312 if disk > maxDisk(user, machine):
313 raise InvalidInput("Too much disk requested")
314 disk = int(disk * 1024)
315 if disk < MIN_DISK_SINGLE * 1024:
318 raise InvalidInput("Invalid disk amount; minimum is %s GB" %
322 def create(user, fields):
323 """Handler for create requests."""
324 name = fields.getfirst('name')
325 if not validMachineName(name):
326 raise InvalidInput("Invalid name '%s'" % name)
327 name = user.username + '_' + name.lower()
329 if Machine.get_by(name=name):
330 raise InvalidInput("A machine named '%s' already exists" % name)
332 memory = fields.getfirst('memory')
333 memory = validMemory(user, memory)
335 disk = fields.getfirst('disk')
336 disk = validDisk(user, disk)
338 vm_type = fields.getfirst('vmtype')
339 if vm_type not in ('hvm', 'paravm'):
340 raise CodeError("Invalid vm type '%s'" % vm_type)
341 is_hvm = (vm_type == 'hvm')
343 cdrom = fields.getfirst('cdrom')
344 if cdrom is not None and not CDROM.get(cdrom):
345 raise CodeError("Invalid cdrom type '%s'" % cdrom)
347 machine = createVm(user, name, memory, disk, is_hvm, cdrom)
350 print Template(file='create.tmpl',
351 searchList=[d, global_dict]);
353 def listVms(user, fields):
354 """Handler for list requests."""
355 machines = [m for m in Machine.select() if haveAccess(user, m)]
358 uptimes = getUptimes(machines)
366 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
368 # status = statusInfo(m)
369 # on[m.name] = status is not None
370 # has_vnc[m.name] = hasVnc(status)
371 max_mem=maxMemory(user, on=on)
372 max_disk=maxDisk(user)
374 can_add_vm=canAddVm(user, on=on),
378 default_disk=min(4.0, max_disk),
382 cdroms=CDROM.select())
383 print Template(file='list.tmpl', searchList=[d, global_dict])
385 def testMachineId(user, machineId, exists=True):
386 """Parse, validate and check authorization for a given machineId.
388 If exists is False, don't check that it exists.
390 if machineId is None:
391 raise CodeError("No machine ID specified")
393 machineId = int(machineId)
395 raise CodeError("Invalid machine ID '%s'" % machineId)
396 machine = Machine.get(machineId)
397 if exists and machine is None:
398 raise CodeError("No such machine ID '%s'" % machineId)
399 if machine is not None and not haveAccess(user, machine):
400 raise CodeError("No access to machine ID '%s'" % machineId)
403 def vnc(user, fields):
406 Note that due to same-domain restrictions, the applet connects to
407 the webserver, which needs to forward those requests to the xen
408 server. The Xen server runs another proxy that (1) authenticates
409 and (2) finds the correct port for the VM.
411 You might want iptables like:
413 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
414 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142
415 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
417 Remember to enable iptables!
418 echo 1 > /proc/sys/net/ipv4/ip_forward
420 machine = testMachineId(user, fields.getfirst('machine_id'))
422 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
425 data["user"] = user.username
426 data["machine"]=machine.name
427 data["expires"]=time.time()+(5*60)
428 pickledData = cPickle.dumps(data)
429 m = hmac.new(TOKEN_KEY, digestmod=sha)
430 m.update(pickledData)
431 token = {'data': pickledData, 'digest': m.digest()}
432 token = cPickle.dumps(token)
433 token = base64.urlsafe_b64encode(token)
437 hostname=os.environ.get('SERVER_NAME', 'localhost'),
439 print Template(file='vnc.tmpl',
440 searchList=[d, global_dict])
442 def getNicInfo(data_dict, machine):
443 """Helper function for info, get data on nics for a machine.
445 Modifies data_dict to include the relevant data, and returns a list
446 of (key, name) pairs to display "name: data_dict[key]" to the user.
448 data_dict['num_nics'] = len(machine.nics)
449 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
450 ('nic%s_mac', 'NIC %s MAC Addr'),
451 ('nic%s_ip', 'NIC %s IP'),
454 for i in range(len(machine.nics)):
455 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
456 data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
457 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
458 data_dict['nic%s_ip' % i] = machine.nics[i].ip
459 if len(machine.nics) == 1:
460 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
463 def getDiskInfo(data_dict, machine):
464 """Helper function for info, get data on disks for a machine.
466 Modifies data_dict to include the relevant data, and returns a list
467 of (key, name) pairs to display "name: data_dict[key]" to the user.
469 data_dict['num_disks'] = len(machine.disks)
470 disk_fields_template = [('%s_size', '%s size')]
472 for disk in machine.disks:
473 name = disk.guest_device_name
474 disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
475 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
478 def deleteVM(machine):
480 transaction = ctx.current.create_transaction()
481 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
483 for nic in machine.nics:
484 nic.machine_id = None
486 ctx.current.save(nic)
487 for disk in machine.disks:
488 ctx.current.delete(disk)
489 ctx.current.delete(machine)
492 transaction.rollback()
494 for mname, dname in delete_disk_pairs:
495 remctl('web', 'lvremove', mname, dname)
496 unregisterMachine(machine)
498 def command(user, fields):
499 """Handler for running commands like boot and delete on a VM."""
500 print time.time()-start_time
501 machine = testMachineId(user, fields.getfirst('machine_id'))
502 action = fields.getfirst('action')
503 cdrom = fields.getfirst('cdrom')
504 print time.time()-start_time
505 if cdrom is not None and not CDROM.get(cdrom):
506 raise CodeError("Invalid cdrom type '%s'" % cdrom)
507 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
508 raise CodeError("Invalid action '%s'" % action)
509 if action == 'Reboot':
510 if cdrom is not None:
511 remctl('reboot', machine.name, cdrom)
513 remctl('reboot', machine.name)
514 elif action == 'Power on':
515 if maxMemory(user) < machine.memory:
516 raise InvalidInput("You don't have enough free RAM quota")
517 bootMachine(machine, cdrom)
518 elif action == 'Power off':
519 remctl('destroy', machine.name)
520 elif action == 'Shutdown':
521 remctl('shutdown', machine.name)
522 elif action == 'Delete VM':
524 print time.time()-start_time
529 print Template(file="command.tmpl", searchList=[d, global_dict])
531 def modify(user, fields):
532 """Handler for modifying attributes of a machine."""
534 machine = testMachineId(user, fields.getfirst('machine_id'))
536 def help(user, fields):
537 """Handler for help messages."""
538 simple = fields.getfirst('simple')
539 subjects = fields.getlist('subject')
541 mapping = dict(paravm_console="""
542 ParaVM machines do not support console access over VNC. To access
543 these machines, you either need to boot with a liveCD and ssh in or
544 hope that the sipb-xen maintainers add support for serial consoles.""",
546 HVM machines use the virtualization features of the processor, while
547 ParaVM machines use Xen's emulation of virtualization features. You
548 want an HVM virtualized machine.""",
549 cpu_weight="""Don't ask us! We're as mystified as you are.""")
556 print Template(file="help.tmpl", searchList=[d, global_dict])
559 def info(user, fields):
560 """Handler for info on a single VM."""
561 machine = testMachineId(user, fields.getfirst('machine_id'))
562 status = statusInfo(machine)
563 has_vnc = hasVnc(status)
565 main_status = dict(name=machine.name,
566 memory=str(machine.memory))
568 main_status = dict(status[1:])
569 start_time = float(main_status.get('start_time', 0))
570 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
571 cpu_time_float = float(main_status.get('cpu_time', 0))
572 cputime = datetime.timedelta(seconds=int(cpu_time_float))
573 display_fields = """name uptime memory state cpu_weight on_reboot
574 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
575 display_fields = [('name', 'Name'),
577 ('contact', 'Contact'),
580 ('uptime', 'uptime'),
581 ('cputime', 'CPU usage'),
584 ('state', 'state (xen format)'),
585 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
586 ('on_reboot', 'Action on VM reboot'),
587 ('on_poweroff', 'Action on VM poweroff'),
588 ('on_crash', 'Action on VM crash'),
589 ('on_xend_start', 'Action on Xen start'),
590 ('on_xend_stop', 'Action on Xen stop'),
591 ('bootloader', 'Bootloader options'),
595 machine_info['name'] = machine.name
596 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
597 machine_info['owner'] = machine.owner
598 machine_info['contact'] = machine.contact
600 nic_fields = getNicInfo(machine_info, machine)
601 nic_point = display_fields.index('NIC_INFO')
602 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
604 disk_fields = getDiskInfo(machine_info, machine)
605 disk_point = display_fields.index('DISK_INFO')
606 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
608 main_status['memory'] += ' MB'
609 for field, disp in display_fields:
610 if field in ('uptime', 'cputime'):
611 fields.append((disp, locals()[field]))
612 elif field in machine_info:
613 fields.append((disp, machine_info[field]))
614 elif field in main_status:
615 fields.append((disp, main_status[field]))
618 #fields.append((disp, None))
619 max_mem = maxMemory(user, machine)
620 max_disk = maxDisk(user, machine)
622 cdroms=CDROM.select(),
623 on=status is not None,
631 print Template(file='info.tmpl',
632 searchList=[d, global_dict])
634 mapping = dict(list=listVms,
642 if __name__ == '__main__':
643 start_time = time.time()
644 fields = cgi.FieldStorage()
647 email = 'moo@cow.com'
649 if 'SSL_CLIENT_S_DN_Email' in os.environ:
650 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
651 u.username = username
652 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
656 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
657 operation = os.environ.get('PATH_INFO', '')
658 #print 'Content-Type: text/plain\n'
661 print "Status: 301 Moved Permanently"
662 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
664 print 'Content-Type: text/html\n'
666 if operation.startswith('/'):
667 operation = operation[1:]
671 fun = mapping.get(operation,
673 error(operation, u, e,
674 "Invalid operation '%s'" % operation))
675 if fun not in (help, ):
676 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
679 except CodeError, err:
680 error(operation, u, fields, err)
681 except InvalidInput, err:
682 error(operation, u, fields, err)