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):
27 return '<span class="helplink"><a href="help?subject='+subj+'&simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
31 global_dict['helppopup'] = helppopup
34 # ... and stolen from xend/uuid.py
36 """Generate a random UUID."""
38 return [ random.randint(0, 255) for _ in range(0, 16) ]
41 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
42 "%02x" * 6]) % tuple(u)
44 MAX_MEMORY_TOTAL = 512
45 MAX_MEMORY_SINGLE = 256
46 MIN_MEMORY_SINGLE = 16
53 def getMachinesOwner(owner):
54 return Machine.select_by(owner=owner)
56 def maxMemory(user, machine=None, on=None):
57 machines = getMachinesOwner(user.username)
59 on = getUptimes(machines)
60 active_machines = [x for x in machines if on[x]]
61 mem_usage = sum([x.memory for x in active_machines if x != machine])
62 return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
64 def maxDisk(user, machine=None):
65 machines = getMachinesOwner(user.username)
66 disk_usage = sum([sum([y.size for y in x.disks])
67 for x in machines if x != machine])
68 return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
70 def canAddVm(user, on=None):
71 machines = getMachinesOwner(user.username)
73 on = getUptimes(machines)
74 active_machines = [x for x in machines if on[x]]
75 return (len(machines) < MAX_VMS_TOTAL and
76 len(active_machines) < MAX_VMS_ACTIVE)
78 def haveAccess(user, machine):
79 if user.username == 'moo':
81 return machine.owner == user.username
83 def error(op, user, fields, err):
84 d = dict(op=op, user=user, errorMessage=str(err))
85 print Template(file='error.tmpl', searchList=[d, global_dict]);
87 def validMachineName(name):
88 """Check that name is valid for a machine name"""
91 charset = string.ascii_letters + string.digits + '-_'
92 if name[0] in '-_' or len(name) > 22:
99 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
100 """Kinit with a given username and keytab"""
102 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
103 stderr=subprocess.PIPE)
106 raise MyException("Error %s in kinit: %s" % (e, p.stderr.read()))
109 """If we lack tickets, kinit."""
110 p = subprocess.Popen(['klist', '-s'])
114 def remctl(*args, **kws):
115 """Perform a remctl and return the output.
117 kinits if necessary, and outputs errors to stderr.
120 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
122 stdout=subprocess.PIPE,
123 stderr=subprocess.PIPE)
126 return p.stdout.read(), p.stderr.read()
128 raise MyException('ERROR on remctl %s: %s' %
129 (args, p.stderr.read()))
130 return p.stdout.read()
133 """Update the lvm partitions to include all disks in the database."""
134 remctl('web', 'lvcreate')
136 def bootMachine(machine, cdtype):
137 """Boot a machine with a given boot CD.
139 If cdtype is None, give no boot cd. Otherwise, it is the string
140 id of the CD (e.g. 'gutsy_i386')
142 if cdtype is not None:
143 remctl('web', 'vmboot', machine.name,
146 remctl('web', 'vmboot', machine.name)
148 def registerMachine(machine):
149 """Register a machine to be controlled by the web interface"""
150 remctl('web', 'register', machine.name)
152 def unregisterMachine(machine):
153 """Unregister a machine to not be controlled by the web interface"""
154 remctl('web', 'unregister', machine.name)
157 """Parse a status string into nested tuples of strings.
159 s = output of xm list --long <machine_name>
161 values = re.split('([()])', s)
163 for v in values[2:-2]: #remove initial and final '()'
170 if len(stack[-1]) == 1:
172 stack[-2].append(stack[-1])
177 stack[-1].extend(v.split())
180 def getUptimes(machines):
181 """Return a dictionary mapping machine names to uptime strings"""
182 value_string = remctl('web', 'listvms')
183 lines = value_string.splitlines()
185 for line in lines[1:]:
188 uptime = ' '.join(lst[2:])
192 ans[m] = d.get(m.name)
195 def statusInfo(machine):
196 """Return the status list for a given machine.
198 Gets and parses xm list --long
200 value_string, err_string = remctl('list-long', machine.name, err=True)
201 if 'Unknown command' in err_string:
202 raise MyException("ERROR in remctl list-long %s is not registered" % (machine.name,))
203 elif 'does not exist' in err_string:
206 raise MyException("ERROR in remctl list-long %s: %s" % (machine.name, err_string))
207 status = parseStatus(value_string)
211 """Does the machine with a given status list support VNC?"""
215 if l[0] == 'device' and l[1][0] == 'vfb':
217 return 'location' in d
220 def createVm(user, name, memory, disk, is_hvm, cdrom):
221 """Create a VM and put it in the database"""
222 # put stuff in the table
223 transaction = ctx.current.create_transaction()
225 if memory > maxMemory(user):
226 raise MyException("Too much memory requested")
227 if disk > maxDisk(user) * 1024:
228 raise MyException("Too much disk requested")
229 if not canAddVm(user):
230 raise MyException("Too many VMs requested")
231 res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
232 id = res.fetchone()[0]
234 machine.machine_id = id
236 machine.memory = memory
237 machine.owner = user.username
238 machine.contact = user.email
239 machine.uuid = uuidToString(randomUUID())
240 machine.boot_off_cd = True
241 machine_type = Type.get_by(hvm=is_hvm)
242 machine.type_id = machine_type.type_id
243 ctx.current.save(machine)
244 disk = Disk(machine.machine_id,
246 open = NIC.select_by(machine_id=None)
247 if not open: #No IPs left!
248 return "No IP addresses left! Contact sipb-xen-dev@mit.edu"
250 nic.machine_id = machine.machine_id
252 ctx.current.save(nic)
253 ctx.current.save(disk)
256 transaction.rollback()
258 registerMachine(machine)
260 # tell it to boot with cdrom
261 bootMachine(machine, cdrom)
265 def validMemory(user, memory, machine=None):
268 if memory < MIN_MEMORY_SINGLE:
271 raise MyException("Invalid memory amount; must be at least %s MB" %
273 if memory > maxMemory(user, machine):
274 raise MyException("Too much memory requested")
277 def validDisk(user, disk, machine=None):
280 if disk > maxDisk(user, machine):
281 raise MyException("Too much disk requested")
282 disk = int(disk * 1024)
283 if disk < MIN_DISK_SINGLE * 1024:
286 raise MyException("Invalid disk amount; minimum is %s GB" %
290 def create(user, fields):
291 name = fields.getfirst('name')
292 if not validMachineName(name):
293 raise MyException("Invalid name '%s'" % name)
294 name = user.username + '_' + name.lower()
296 if Machine.get_by(name=name):
297 raise MyException("A machine named '%s' already exists" % name)
299 memory = fields.getfirst('memory')
300 memory = validMemory(user, memory)
302 disk = fields.getfirst('disk')
303 disk = validDisk(user, disk)
305 vm_type = fields.getfirst('vmtype')
306 if vm_type not in ('hvm', 'paravm'):
307 raise MyException("Invalid vm type '%s'" % vm_type)
308 is_hvm = (vm_type == 'hvm')
310 cdrom = fields.getfirst('cdrom')
311 if cdrom is not None and not CDROM.get(cdrom):
312 raise MyException("Invalid cdrom type '%s'" % cdrom)
314 machine = createVm(user, name, memory, disk, is_hvm, cdrom)
315 if isinstance(machine, basestring):
316 raise MyException(machine)
319 print Template(file='create.tmpl',
320 searchList=[d, global_dict]);
322 def listVms(user, fields):
323 machines = [m for m in Machine.select() if haveAccess(user, m)]
326 uptimes = getUptimes(machines)
334 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
336 # status = statusInfo(m)
337 # on[m.name] = status is not None
338 # has_vnc[m.name] = hasVnc(status)
339 max_mem=maxMemory(user, on=on)
340 max_disk=maxDisk(user)
342 can_add_vm=canAddVm(user, on=on),
346 default_disk=min(4.0, max_disk),
350 cdroms=CDROM.select())
351 print Template(file='list.tmpl', searchList=[d, global_dict])
353 def testMachineId(user, machineId, exists=True):
354 if machineId is None:
355 raise MyException("No machine ID specified")
357 machineId = int(machineId)
359 raise MyException("Invalid machine ID '%s'" % machineId)
360 machine = Machine.get(machineId)
361 if exists and machine is None:
362 raise MyException("No such machine ID '%s'" % machineId)
363 if not haveAccess(user, machine):
364 raise MyException("No access to machine ID '%s'" % machineId)
367 def vnc(user, fields):
370 Note that due to same-domain restrictions, the applet connects to
371 the webserver, which needs to forward those requests to the xen
372 server. The Xen server runs another proxy that (1) authenticates
373 and (2) finds the correct port for the VM.
375 You might want iptables like:
377 -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
378 -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
379 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
381 machine = testMachineId(user, fields.getfirst('machine_id'))
384 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
387 data["user"] = user.username
388 data["machine"]=machine.name
389 data["expires"]=time.time()+(5*60)
390 pickledData = cPickle.dumps(data)
391 m = hmac.new(TOKEN_KEY, digestmod=sha)
392 m.update(pickledData)
393 token = {'data': pickledData, 'digest': m.digest()}
394 token = cPickle.dumps(token)
395 token = base64.urlsafe_b64encode(token)
399 hostname=os.environ.get('SERVER_NAME', 'localhost'),
401 print Template(file='vnc.tmpl',
402 searchList=[d, global_dict])
404 def getNicInfo(data_dict, machine):
405 data_dict['num_nics'] = len(machine.nics)
406 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
407 ('nic%s_mac', 'NIC %s MAC Addr'),
408 ('nic%s_ip', 'NIC %s IP'),
411 for i in range(len(machine.nics)):
412 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
413 data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
414 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
415 data_dict['nic%s_ip' % i] = machine.nics[i].ip
416 if len(machine.nics) == 1:
417 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
420 def getDiskInfo(data_dict, machine):
421 data_dict['num_disks'] = len(machine.disks)
422 disk_fields_template = [('%s_size', '%s size')]
424 for disk in machine.disks:
425 name = disk.guest_device_name
426 disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
427 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
430 def deleteVM(machine):
431 transaction = ctx.current.create_transaction()
432 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
434 for nic in machine.nics:
435 nic.machine_id = None
437 ctx.current.save(nic)
438 for disk in machine.disks:
439 ctx.current.delete(disk)
440 ctx.current.delete(machine)
443 transaction.rollback()
445 for mname, dname in delete_disk_pairs:
446 remctl('web', 'lvremove', mname, dname)
447 unregisterMachine(machine)
449 def command(user, fields):
450 print time.time()-start_time
451 machine = testMachineId(user, fields.getfirst('machine_id'))
452 action = fields.getfirst('action')
453 cdrom = fields.getfirst('cdrom')
454 print time.time()-start_time
455 if cdrom is not None and not CDROM.get(cdrom):
456 raise MyException("Invalid cdrom type '%s'" % cdrom)
457 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
458 raise MyException("Invalid action '%s'" % action)
459 if action == 'Reboot':
460 if cdrom is not None:
461 remctl('reboot', machine.name, cdrom)
463 remctl('reboot', machine.name)
464 elif action == 'Power on':
465 if maxMemory(user) < machine.memory:
466 raise MyException("You don't have enough free RAM quota")
467 bootMachine(machine, cdrom)
468 elif action == 'Power off':
469 remctl('destroy', machine.name)
470 elif action == 'Shutdown':
471 remctl('shutdown', machine.name)
472 elif action == 'Delete VM':
474 print time.time()-start_time
479 print Template(file="command.tmpl", searchList=[d, global_dict])
481 def modify(user, fields):
482 machine = testMachineId(user, fields.getfirst('machine_id'))
484 def help(user, fields):
485 simple = fields.getfirst('simple')
486 subjects = fields.getlist('subject')
488 mapping = dict(paravm_console="""
489 ParaVM machines do not support console access over VNC. To access
490 these machines, you either need to boot with a liveCD and ssh in or
491 hope that the sipb-xen maintainers add support for serial consoles.""",
493 HVM machines use the virtualization features of the processor, while
494 ParaVM machines use Xen's emulation of virtualization features. You
495 want an HVM virtualized machine.""",
496 cpu_weight="""Don't ask us! We're as mystified as you are.""")
503 print Template(file="help.tmpl", searchList=[d, global_dict])
506 def info(user, fields):
507 machine = testMachineId(user, fields.getfirst('machine_id'))
508 status = statusInfo(machine)
509 has_vnc = hasVnc(status)
511 main_status = dict(name=machine.name,
512 memory=str(machine.memory))
514 main_status = dict(status[1:])
515 start_time = float(main_status.get('start_time', 0))
516 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
517 cpu_time_float = float(main_status.get('cpu_time', 0))
518 cputime = datetime.timedelta(seconds=int(cpu_time_float))
519 display_fields = """name uptime memory state cpu_weight on_reboot
520 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
521 display_fields = [('name', 'Name'),
523 ('contact', 'Contact'),
526 ('uptime', 'uptime'),
527 ('cputime', 'CPU usage'),
530 ('state', 'state (xen format)'),
531 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
532 ('on_reboot', 'Action on VM reboot'),
533 ('on_poweroff', 'Action on VM poweroff'),
534 ('on_crash', 'Action on VM crash'),
535 ('on_xend_start', 'Action on Xen start'),
536 ('on_xend_stop', 'Action on Xen stop'),
537 ('bootloader', 'Bootloader options'),
541 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
542 machine_info['owner'] = machine.owner
543 machine_info['contact'] = machine.contact
545 nic_fields = getNicInfo(machine_info, machine)
546 nic_point = display_fields.index('NIC_INFO')
547 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
549 disk_fields = getDiskInfo(machine_info, machine)
550 disk_point = display_fields.index('DISK_INFO')
551 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
553 main_status['memory'] += ' MB'
554 for field, disp in display_fields:
555 if field in ('uptime', 'cputime'):
556 fields.append((disp, locals()[field]))
557 elif field in main_status:
558 fields.append((disp, main_status[field]))
559 elif field in machine_info:
560 fields.append((disp, machine_info[field]))
563 #fields.append((disp, None))
564 max_mem = maxMemory(user, machine)
565 max_disk = maxDisk(user, machine)
567 cdroms=CDROM.select(),
568 on=status is not None,
576 print Template(file='info.tmpl',
577 searchList=[d, global_dict])
579 mapping = dict(list=listVms,
587 if __name__ == '__main__':
588 start_time = time.time()
589 fields = cgi.FieldStorage()
592 email = 'moo@cow.com'
594 if 'SSL_CLIENT_S_DN_Email' in os.environ:
595 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
596 u.username = username
597 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
601 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
602 operation = os.environ.get('PATH_INFO', '')
603 #print 'Content-Type: text/plain\n'
606 print "Status: 301 Moved Permanently"
607 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
609 print 'Content-Type: text/html\n'
611 if operation.startswith('/'):
612 operation = operation[1:]
616 fun = mapping.get(operation,
618 error(operation, u, e,
619 "Invalid operation '%s'" % operation))
620 if fun not in (help, ):
621 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
624 except MyException, err:
625 error(operation, u, fields, err)