16 print 'Content-Type: text/html\n'
17 sys.stderr = sys.stdout
18 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
20 from Cheetah.Template import Template
21 from sipb_xen_database import *
24 class MyException(Exception):
28 return '<span class="helplink"><a href="help?subject='+subj+'&simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
32 global_dict['helppopup'] = helppopup
35 # ... and stolen from xend/uuid.py
37 """Generate a random UUID."""
39 return [ random.randint(0, 255) for _ in range(0, 16) ]
42 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
43 "%02x" * 6]) % tuple(u)
45 def maxMemory(user, machine=None):
48 def maxDisk(user, machine=None):
51 def haveAccess(user, machine):
52 if user.username == 'moo':
54 return machine.owner == user.username
56 def error(op, user, fields, err):
57 d = dict(op=op, user=user, errorMessage=str(err))
58 print Template(file='error.tmpl', searchList=[d, global_dict]);
60 def validMachineName(name):
61 """Check that name is valid for a machine name"""
64 charset = string.ascii_letters + string.digits + '-_'
65 if name[0] in '-_' or len(name) > 22:
67 return all(x in charset for x in name)
69 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
70 """Kinit with a given username and keytab"""
72 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
73 stderr=subprocess.PIPE)
76 raise MyException("Error %s in kinit: %s" % (e, p.stderr.read()))
79 """If we lack tickets, kinit."""
80 p = subprocess.Popen(['klist', '-s'])
84 def remctl(*args, **kws):
85 """Perform a remctl and return the output.
87 kinits if necessary, and outputs errors to stderr.
90 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
92 stdout=subprocess.PIPE,
93 stderr=subprocess.PIPE)
95 return p.stdout.read(), p.stderr.read()
97 print >> sys.stderr, 'ERROR on remctl ', args
98 print >> sys.stderr, p.stderr.read()
99 return p.stdout.read()
102 """Update the lvm partitions to include all disks in the database."""
103 remctl('web', 'lvcreate')
105 def bootMachine(machine, cdtype):
106 """Boot a machine with a given boot CD.
108 If cdtype is None, give no boot cd. Otherwise, it is the string
109 id of the CD (e.g. 'gutsy_i386')
111 if cdtype is not None:
112 remctl('web', 'vmboot', machine.name,
115 remctl('web', 'vmboot', machine.name)
117 def registerMachine(machine):
118 """Register a machine to be controlled by the web interface"""
119 remctl('web', 'register', machine.name)
121 def unregisterMachine(machine):
122 """Unregister a machine to not be controlled by the web interface"""
123 remctl('web', 'unregister', machine.name)
126 """Parse a status string into nested tuples of strings.
128 s = output of xm list --long <machine_name>
130 values = re.split('([()])', s)
132 for v in values[2:-2]: #remove initial and final '()'
139 if len(stack[-1]) == 1:
141 stack[-2].append(stack[-1])
146 stack[-1].extend(v.split())
149 def getUptimes(machines):
150 """Return a dictionary mapping machine names to uptime strings"""
151 value_string = remctl('web', 'listvms')
152 lines = value_string.splitlines()
154 for line in lines[1:]:
157 uptime = ' '.join(lst[2:])
161 def statusInfo(machine):
162 """Return the status list for a given machine.
164 Gets and parses xm list --long
166 value_string, err_string = remctl('list-long', machine.name, err=True)
167 if 'Unknown command' in err_string:
168 raise MyException("ERROR in remctl list-long %s is not registered" % (machine.name,))
169 elif 'does not exist' in err_string:
172 raise MyException("ERROR in remctl list-long %s: %s" % (machine.name, err_string))
173 status = parseStatus(value_string)
177 """Does the machine with a given status list support VNC?"""
181 if l[0] == 'device' and l[1][0] == 'vfb':
183 return 'location' in d
186 def createVm(user, name, memory, disk, is_hvm, cdrom):
187 """Create a VM and put it in the database"""
188 # put stuff in the table
189 transaction = ctx.current.create_transaction()
191 res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
192 id = res.fetchone()[0]
194 machine.machine_id = id
196 machine.memory = memory
197 machine.owner = user.username
198 machine.contact = user.email
199 machine.uuid = uuidToString(randomUUID())
200 machine.boot_off_cd = True
201 machine_type = Type.get_by(hvm=is_hvm)
202 machine.type_id = machine_type.type_id
203 ctx.current.save(machine)
204 disk = Disk(machine.machine_id,
206 open = NIC.select_by(machine_id=None)
207 if not open: #No IPs left!
208 return "No IP addresses left! Contact sipb-xen-dev@mit.edu"
210 nic.machine_id = machine.machine_id
212 ctx.current.save(nic)
213 ctx.current.save(disk)
216 transaction.rollback()
219 registerMachine(machine)
220 # tell it to boot with cdrom
221 bootMachine(machine, cdrom)
225 def validMemory(user, memory, machine=None):
231 raise MyException("Invalid memory amount")
232 if memory > maxMemory(user, machine):
233 raise MyException("Too much memory requested")
236 def validDisk(user, disk, machine=None):
239 if disk > maxDisk(user, machine):
240 raise MyException("Too much disk requested")
241 disk = int(disk * 1024)
245 raise MyException("Invalid disk amount")
248 def create(user, fields):
249 name = fields.getfirst('name')
250 if not validMachineName(name):
251 raise MyException("Invalid name '%s'" % name)
252 name = user.username + '_' + name.lower()
254 if Machine.get_by(name=name):
255 raise MyException("A machine named '%s' already exists" % name)
257 memory = fields.getfirst('memory')
258 memory = validMemory(user, memory)
260 disk = fields.getfirst('disk')
261 disk = validDisk(user, disk)
263 vm_type = fields.getfirst('vmtype')
264 if vm_type not in ('hvm', 'paravm'):
265 raise MyException("Invalid vm type '%s'" % vm_type)
266 is_hvm = (vm_type == 'hvm')
268 cdrom = fields.getfirst('cdrom')
269 if cdrom is not None and not CDROM.get(cdrom):
270 raise MyException("Invalid cdrom type '%s'" % cdrom)
272 machine = createVm(user, name, memory, disk, is_hvm, cdrom)
273 if isinstance(machine, basestring):
274 raise MyException(machine)
277 print Template(file='create.tmpl',
278 searchList=[d, global_dict]);
280 def listVms(user, fields):
281 machines = [m for m in Machine.select() if haveAccess(user, m)]
284 uptimes = getUptimes(machines)
287 if not on.get(m.name):
288 has_vnc[m.name] = 'Off'
290 has_vnc[m.name] = True
292 has_vnc[m.name] = "ParaVM"+helppopup("paravm_console")
294 # status = statusInfo(m)
295 # on[m.name] = status is not None
296 # has_vnc[m.name] = hasVnc(status)
298 maxmem=maxMemory(user),
299 maxdisk=maxDisk(user),
303 cdroms=CDROM.select())
304 print Template(file='list.tmpl', searchList=[d, global_dict])
306 def testMachineId(user, machineId, exists=True):
307 if machineId is None:
308 raise MyException("No machine ID specified")
310 machineId = int(machineId)
312 raise MyException("Invalid machine ID '%s'" % machineId)
313 machine = Machine.get(machineId)
314 if exists and machine is None:
315 raise MyException("No such machine ID '%s'" % machineId)
316 if not haveAccess(user, machine):
317 raise MyException("No access to machine ID '%s'" % machineId)
320 def vnc(user, fields):
323 Note that due to same-domain restrictions, the applet connects to
324 the webserver, which needs to forward those requests to the xen
325 server. The Xen server runs another proxy that (1) authenticates
326 and (2) finds the correct port for the VM.
328 You might want iptables like:
330 -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
331 -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
332 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
334 machine = testMachineId(user, fields.getfirst('machine_id'))
337 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
340 data["user"] = user.username
341 data["machine"]=machine.name
342 data["expires"]=time.time()+(5*60)
343 pickledData = cPickle.dumps(data)
344 m = hmac.new(TOKEN_KEY, digestmod=sha)
345 m.update(pickledData)
346 token = {'data': pickledData, 'digest': m.digest()}
347 token = cPickle.dumps(token)
348 token = base64.urlsafe_b64encode(token)
352 hostname=os.environ.get('SERVER_NAME', 'localhost'),
354 print Template(file='vnc.tmpl',
355 searchList=[d, global_dict])
357 def getNicInfo(data_dict, machine):
358 data_dict['num_nics'] = len(machine.nics)
359 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
360 ('nic%s_mac', 'NIC %s MAC Addr'),
361 ('nic%s_ip', 'NIC %s IP'),
364 for i in range(len(machine.nics)):
365 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
366 data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
367 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
368 data_dict['nic%s_ip' % i] = machine.nics[i].ip
369 if len(machine.nics) == 1:
370 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
373 def getDiskInfo(data_dict, machine):
374 data_dict['num_disks'] = len(machine.disks)
375 disk_fields_template = [('%s_size', '%s size')]
377 for disk in machine.disks:
378 name = disk.guest_device_name
379 disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
380 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
383 def deleteVM(machine):
384 transaction = ctx.current.create_transaction()
385 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
387 for nic in machine.nics:
388 nic.machine_id = None
390 ctx.current.save(nic)
391 for disk in machine.disks:
392 ctx.current.delete(disk)
393 ctx.current.delete(machine)
396 transaction.rollback()
398 for mname, dname in delete_disk_pairs:
399 remctl('web', 'lvremove', mname, dname)
400 unregisterMachine(machine)
402 def command(user, fields):
403 print time.time()-start_time
404 machine = testMachineId(user, fields.getfirst('machine_id'))
405 action = fields.getfirst('action')
406 cdrom = fields.getfirst('cdrom')
407 print time.time()-start_time
408 if cdrom is not None and not CDROM.get(cdrom):
409 raise MyException("Invalid cdrom type '%s'" % cdrom)
410 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
411 raise MyException("Invalid action '%s'" % action)
412 if action == 'Reboot':
413 if cdrom is not None:
414 remctl('reboot', machine.name, cdrom)
416 remctl('reboot', machine.name)
417 elif action == 'Power on':
418 bootMachine(machine, cdrom)
419 elif action == 'Power off':
420 remctl('destroy', machine.name)
421 elif action == 'Shutdown':
422 remctl('shutdown', machine.name)
423 elif action == 'Delete VM':
425 print time.time()-start_time
430 print Template(file="command.tmpl", searchList=[d, global_dict])
432 def modify(user, fields):
433 machine = testMachineId(user, fields.getfirst('machine_id'))
435 def help(user, fields):
436 simple = fields.getfirst('simple')
437 subjects = fields.getlist('subject')
439 mapping = dict(paravm_console="""
440 ParaVM machines do not support console access over VNC. To access
441 these machines, you either need to boot with a liveCD and ssh in or
442 hope that the sipb-xen maintainers add support for serial consoles.""",
444 HVM machines use the virtualization features of the processor, while
445 ParaVM machines use Xen's emulation of virtualization features. You
446 want an HVM virtualized machine.""",
447 cpu_weight="""Don't ask us! We're as mystified as you are.""")
454 print Template(file="help.tmpl", searchList=[d, global_dict])
457 def info(user, fields):
458 machine = testMachineId(user, fields.getfirst('machine_id'))
459 status = statusInfo(machine)
460 has_vnc = hasVnc(status)
462 main_status = dict(name=machine.name,
463 memory=str(machine.memory))
465 main_status = dict(status[1:])
466 start_time = float(main_status.get('start_time', 0))
467 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
468 cpu_time_float = float(main_status.get('cpu_time', 0))
469 cputime = datetime.timedelta(seconds=int(cpu_time_float))
470 display_fields = """name uptime memory state cpu_weight on_reboot
471 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
472 display_fields = [('name', 'Name'),
474 ('contact', 'Contact'),
477 ('uptime', 'uptime'),
478 ('cputime', 'CPU usage'),
481 ('state', 'state (xen format)'),
482 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
483 ('on_reboot', 'Action on VM reboot'),
484 ('on_poweroff', 'Action on VM poweroff'),
485 ('on_crash', 'Action on VM crash'),
486 ('on_xend_start', 'Action on Xen start'),
487 ('on_xend_stop', 'Action on Xen stop'),
488 ('bootloader', 'Bootloader options'),
492 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
493 machine_info['owner'] = machine.owner
494 machine_info['contact'] = machine.contact
496 nic_fields = getNicInfo(machine_info, machine)
497 nic_point = display_fields.index('NIC_INFO')
498 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
500 disk_fields = getDiskInfo(machine_info, machine)
501 disk_point = display_fields.index('DISK_INFO')
502 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
504 main_status['memory'] += ' MB'
505 for field, disp in display_fields:
506 if field in ('uptime', 'cputime'):
507 fields.append((disp, locals()[field]))
508 elif field in main_status:
509 fields.append((disp, main_status[field]))
510 elif field in machine_info:
511 fields.append((disp, machine_info[field]))
514 #fields.append((disp, None))
517 cdroms=CDROM.select(),
518 on=status is not None,
523 maxmem=maxMemory(user, machine),
524 maxdisk=maxDisk(user, machine),
526 print Template(file='info.tmpl',
527 searchList=[d, global_dict])
529 mapping = dict(list=listVms,
537 if __name__ == '__main__':
538 start_time = time.time()
539 fields = cgi.FieldStorage()
542 email = 'moo@cow.com'
544 operation = os.environ.get('PATH_INFO', '')
549 if operation.startswith('/'):
550 operation = operation[1:]
554 fun = mapping.get(operation,
556 error(operation, u, e,
557 "Invalid operation '%s'" % operation))
558 if fun not in (help, ):
559 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
562 except MyException, err:
563 error(operation, u, fields, err)