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 def maxMemory(user, machine=None):
47 def maxDisk(user, machine=None):
50 def haveAccess(user, machine):
51 if user.username == 'moo':
53 return machine.owner == user.username
55 def error(op, user, fields, err):
56 d = dict(op=op, user=user, errorMessage=str(err))
57 print Template(file='error.tmpl', searchList=[d, global_dict]);
59 def validMachineName(name):
60 """Check that name is valid for a machine name"""
63 charset = string.ascii_letters + string.digits + '-_'
64 if name[0] in '-_' or len(name) > 22:
71 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
72 """Kinit with a given username and keytab"""
74 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
75 stderr=subprocess.PIPE)
78 raise MyException("Error %s in kinit: %s" % (e, p.stderr.read()))
81 """If we lack tickets, kinit."""
82 p = subprocess.Popen(['klist', '-s'])
86 def remctl(*args, **kws):
87 """Perform a remctl and return the output.
89 kinits if necessary, and outputs errors to stderr.
92 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
94 stdout=subprocess.PIPE,
95 stderr=subprocess.PIPE)
97 return p.stdout.read(), p.stderr.read()
99 print >> sys.stderr, 'ERROR on remctl ', args
100 print >> sys.stderr, p.stderr.read()
101 return p.stdout.read()
104 """Update the lvm partitions to include all disks in the database."""
105 remctl('web', 'lvcreate')
107 def bootMachine(machine, cdtype):
108 """Boot a machine with a given boot CD.
110 If cdtype is None, give no boot cd. Otherwise, it is the string
111 id of the CD (e.g. 'gutsy_i386')
113 if cdtype is not None:
114 remctl('web', 'vmboot', machine.name,
117 remctl('web', 'vmboot', machine.name)
119 def registerMachine(machine):
120 """Register a machine to be controlled by the web interface"""
121 remctl('web', 'register', machine.name)
123 def unregisterMachine(machine):
124 """Unregister a machine to not be controlled by the web interface"""
125 remctl('web', 'unregister', machine.name)
128 """Parse a status string into nested tuples of strings.
130 s = output of xm list --long <machine_name>
132 values = re.split('([()])', s)
134 for v in values[2:-2]: #remove initial and final '()'
141 if len(stack[-1]) == 1:
143 stack[-2].append(stack[-1])
148 stack[-1].extend(v.split())
151 def getUptimes(machines):
152 """Return a dictionary mapping machine names to uptime strings"""
153 value_string = remctl('web', 'listvms')
154 lines = value_string.splitlines()
156 for line in lines[1:]:
159 uptime = ' '.join(lst[2:])
163 def statusInfo(machine):
164 """Return the status list for a given machine.
166 Gets and parses xm list --long
168 value_string, err_string = remctl('list-long', machine.name, err=True)
169 if 'Unknown command' in err_string:
170 raise MyException("ERROR in remctl list-long %s is not registered" % (machine.name,))
171 elif 'does not exist' in err_string:
174 raise MyException("ERROR in remctl list-long %s: %s" % (machine.name, err_string))
175 status = parseStatus(value_string)
179 """Does the machine with a given status list support VNC?"""
183 if l[0] == 'device' and l[1][0] == 'vfb':
185 return 'location' in d
188 def createVm(user, name, memory, disk, is_hvm, cdrom):
189 """Create a VM and put it in the database"""
190 # put stuff in the table
191 transaction = ctx.current.create_transaction()
193 res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
194 id = res.fetchone()[0]
196 machine.machine_id = id
198 machine.memory = memory
199 machine.owner = user.username
200 machine.contact = user.email
201 machine.uuid = uuidToString(randomUUID())
202 machine.boot_off_cd = True
203 machine_type = Type.get_by(hvm=is_hvm)
204 machine.type_id = machine_type.type_id
205 ctx.current.save(machine)
206 disk = Disk(machine.machine_id,
208 open = NIC.select_by(machine_id=None)
209 if not open: #No IPs left!
210 return "No IP addresses left! Contact sipb-xen-dev@mit.edu"
212 nic.machine_id = machine.machine_id
214 ctx.current.save(nic)
215 ctx.current.save(disk)
218 transaction.rollback()
221 registerMachine(machine)
222 # tell it to boot with cdrom
223 bootMachine(machine, cdrom)
227 def validMemory(user, memory, machine=None):
233 raise MyException("Invalid memory amount")
234 if memory > maxMemory(user, machine):
235 raise MyException("Too much memory requested")
238 def validDisk(user, disk, machine=None):
241 if disk > maxDisk(user, machine):
242 raise MyException("Too much disk requested")
243 disk = int(disk * 1024)
247 raise MyException("Invalid disk amount")
250 def create(user, fields):
251 name = fields.getfirst('name')
252 if not validMachineName(name):
253 raise MyException("Invalid name '%s'" % name)
254 name = user.username + '_' + name.lower()
256 if Machine.get_by(name=name):
257 raise MyException("A machine named '%s' already exists" % name)
259 memory = fields.getfirst('memory')
260 memory = validMemory(user, memory)
262 disk = fields.getfirst('disk')
263 disk = validDisk(user, disk)
265 vm_type = fields.getfirst('vmtype')
266 if vm_type not in ('hvm', 'paravm'):
267 raise MyException("Invalid vm type '%s'" % vm_type)
268 is_hvm = (vm_type == 'hvm')
270 cdrom = fields.getfirst('cdrom')
271 if cdrom is not None and not CDROM.get(cdrom):
272 raise MyException("Invalid cdrom type '%s'" % cdrom)
274 machine = createVm(user, name, memory, disk, is_hvm, cdrom)
275 if isinstance(machine, basestring):
276 raise MyException(machine)
279 print Template(file='create.tmpl',
280 searchList=[d, global_dict]);
282 def listVms(user, fields):
283 machines = [m for m in Machine.select() if haveAccess(user, m)]
286 uptimes = getUptimes(machines)
289 if not on.get(m.name):
290 has_vnc[m.name] = 'Off'
292 has_vnc[m.name] = True
294 has_vnc[m.name] = "ParaVM"+helppopup("paravm_console")
296 # status = statusInfo(m)
297 # on[m.name] = status is not None
298 # has_vnc[m.name] = hasVnc(status)
300 maxmem=maxMemory(user),
301 maxdisk=maxDisk(user),
305 cdroms=CDROM.select())
306 print Template(file='list.tmpl', searchList=[d, global_dict])
308 def testMachineId(user, machineId, exists=True):
309 if machineId is None:
310 raise MyException("No machine ID specified")
312 machineId = int(machineId)
314 raise MyException("Invalid machine ID '%s'" % machineId)
315 machine = Machine.get(machineId)
316 if exists and machine is None:
317 raise MyException("No such machine ID '%s'" % machineId)
318 if not haveAccess(user, machine):
319 raise MyException("No access to machine ID '%s'" % machineId)
322 def vnc(user, fields):
325 Note that due to same-domain restrictions, the applet connects to
326 the webserver, which needs to forward those requests to the xen
327 server. The Xen server runs another proxy that (1) authenticates
328 and (2) finds the correct port for the VM.
330 You might want iptables like:
332 -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
333 -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
334 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
336 machine = testMachineId(user, fields.getfirst('machine_id'))
339 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
342 data["user"] = user.username
343 data["machine"]=machine.name
344 data["expires"]=time.time()+(5*60)
345 pickledData = cPickle.dumps(data)
346 m = hmac.new(TOKEN_KEY, digestmod=sha)
347 m.update(pickledData)
348 token = {'data': pickledData, 'digest': m.digest()}
349 token = cPickle.dumps(token)
350 token = base64.urlsafe_b64encode(token)
354 hostname=os.environ.get('SERVER_NAME', 'localhost'),
356 print Template(file='vnc.tmpl',
357 searchList=[d, global_dict])
359 def getNicInfo(data_dict, machine):
360 data_dict['num_nics'] = len(machine.nics)
361 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
362 ('nic%s_mac', 'NIC %s MAC Addr'),
363 ('nic%s_ip', 'NIC %s IP'),
366 for i in range(len(machine.nics)):
367 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
368 data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
369 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
370 data_dict['nic%s_ip' % i] = machine.nics[i].ip
371 if len(machine.nics) == 1:
372 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
375 def getDiskInfo(data_dict, machine):
376 data_dict['num_disks'] = len(machine.disks)
377 disk_fields_template = [('%s_size', '%s size')]
379 for disk in machine.disks:
380 name = disk.guest_device_name
381 disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
382 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
385 def deleteVM(machine):
386 transaction = ctx.current.create_transaction()
387 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
389 for nic in machine.nics:
390 nic.machine_id = None
392 ctx.current.save(nic)
393 for disk in machine.disks:
394 ctx.current.delete(disk)
395 ctx.current.delete(machine)
398 transaction.rollback()
400 for mname, dname in delete_disk_pairs:
401 remctl('web', 'lvremove', mname, dname)
402 unregisterMachine(machine)
404 def command(user, fields):
405 print time.time()-start_time
406 machine = testMachineId(user, fields.getfirst('machine_id'))
407 action = fields.getfirst('action')
408 cdrom = fields.getfirst('cdrom')
409 print time.time()-start_time
410 if cdrom is not None and not CDROM.get(cdrom):
411 raise MyException("Invalid cdrom type '%s'" % cdrom)
412 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
413 raise MyException("Invalid action '%s'" % action)
414 if action == 'Reboot':
415 if cdrom is not None:
416 remctl('reboot', machine.name, cdrom)
418 remctl('reboot', machine.name)
419 elif action == 'Power on':
420 bootMachine(machine, cdrom)
421 elif action == 'Power off':
422 remctl('destroy', machine.name)
423 elif action == 'Shutdown':
424 remctl('shutdown', machine.name)
425 elif action == 'Delete VM':
427 print time.time()-start_time
432 print Template(file="command.tmpl", searchList=[d, global_dict])
434 def modify(user, fields):
435 machine = testMachineId(user, fields.getfirst('machine_id'))
437 def help(user, fields):
438 simple = fields.getfirst('simple')
439 subjects = fields.getlist('subject')
441 mapping = dict(paravm_console="""
442 ParaVM machines do not support console access over VNC. To access
443 these machines, you either need to boot with a liveCD and ssh in or
444 hope that the sipb-xen maintainers add support for serial consoles.""",
446 HVM machines use the virtualization features of the processor, while
447 ParaVM machines use Xen's emulation of virtualization features. You
448 want an HVM virtualized machine.""",
449 cpu_weight="""Don't ask us! We're as mystified as you are.""")
456 print Template(file="help.tmpl", searchList=[d, global_dict])
459 def info(user, fields):
460 machine = testMachineId(user, fields.getfirst('machine_id'))
461 status = statusInfo(machine)
462 has_vnc = hasVnc(status)
464 main_status = dict(name=machine.name,
465 memory=str(machine.memory))
467 main_status = dict(status[1:])
468 start_time = float(main_status.get('start_time', 0))
469 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
470 cpu_time_float = float(main_status.get('cpu_time', 0))
471 cputime = datetime.timedelta(seconds=int(cpu_time_float))
472 display_fields = """name uptime memory state cpu_weight on_reboot
473 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
474 display_fields = [('name', 'Name'),
476 ('contact', 'Contact'),
479 ('uptime', 'uptime'),
480 ('cputime', 'CPU usage'),
483 ('state', 'state (xen format)'),
484 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
485 ('on_reboot', 'Action on VM reboot'),
486 ('on_poweroff', 'Action on VM poweroff'),
487 ('on_crash', 'Action on VM crash'),
488 ('on_xend_start', 'Action on Xen start'),
489 ('on_xend_stop', 'Action on Xen stop'),
490 ('bootloader', 'Bootloader options'),
494 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
495 machine_info['owner'] = machine.owner
496 machine_info['contact'] = machine.contact
498 nic_fields = getNicInfo(machine_info, machine)
499 nic_point = display_fields.index('NIC_INFO')
500 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
502 disk_fields = getDiskInfo(machine_info, machine)
503 disk_point = display_fields.index('DISK_INFO')
504 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
506 main_status['memory'] += ' MB'
507 for field, disp in display_fields:
508 if field in ('uptime', 'cputime'):
509 fields.append((disp, locals()[field]))
510 elif field in main_status:
511 fields.append((disp, main_status[field]))
512 elif field in machine_info:
513 fields.append((disp, machine_info[field]))
516 #fields.append((disp, None))
519 cdroms=CDROM.select(),
520 on=status is not None,
525 maxmem=maxMemory(user, machine),
526 maxdisk=maxDisk(user, machine),
528 print Template(file='info.tmpl',
529 searchList=[d, global_dict])
531 mapping = dict(list=listVms,
539 if __name__ == '__main__':
540 start_time = time.time()
541 fields = cgi.FieldStorage()
544 email = 'moo@cow.com'
546 if 'SSL_CLIENT_S_DN_Email' in os.environ:
547 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
548 u.username = username
549 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
551 u.username = 'nobody'
553 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
554 operation = os.environ.get('PATH_INFO', '')
555 #print 'Content-Type: text/plain\n'
558 print "Status: 301 Moved Permanently"
559 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
561 print 'Content-Type: text/html\n'
563 if operation.startswith('/'):
564 operation = operation[1:]
568 fun = mapping.get(operation,
570 error(operation, u, e,
571 "Invalid operation '%s'" % operation))
572 if fun not in (help, ):
573 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
576 except MyException, err:
577 error(operation, u, fields, err)