Machine name cannot be changed while the machine is running without
[invirt/packages/invirt-web.git] / templates / main.py
1 #!/usr/bin/python
2
3 import sys
4 import cgi
5 import os
6 import string
7 import subprocess
8 import re
9 import time
10 import cPickle
11 import base64
12 import sha
13 import hmac
14 import datetime
15 import StringIO
16 import getafsgroups
17
18 sys.stderr = StringIO.StringIO()
19 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
20
21 from Cheetah.Template import Template
22 from sipb_xen_database import *
23 import random
24
25 class MyException(Exception):
26     """Base class for my exceptions"""
27     pass
28
29 class InvalidInput(MyException):
30     """Exception for user-provided input is invalid but maybe in good faith.
31
32     This would include setting memory to negative (which might be a
33     typo) but not setting an invalid boot CD (which requires bypassing
34     the select box).
35     """
36     def __init__(self, err_field, err_value, expl=None):
37         MyException.__init__(self, expl)
38         self.err_field = err_field
39         self.err_value = err_value
40
41 class CodeError(MyException):
42     """Exception for internal errors or bad faith input."""
43     pass
44
45 class Global(object):
46     def __init__(self, user):
47         self.user = user
48
49     def __get_uptimes(self):
50         if not hasattr(self, '_uptimes'):
51             self._uptimes = getUptimes(Machine.select())
52         return self._uptimes
53     uptimes = property(__get_uptimes)
54
55 g = None
56
57 def helppopup(subj):
58     """Return HTML code for a (?) link to a specified help topic"""
59     return '<span class="helplink"><a href="help?subject='+subj+'&amp;simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
60
61
62 global_dict = {}
63 global_dict['helppopup'] = helppopup
64
65
66 # ... and stolen from xend/uuid.py
67 def randomUUID():
68     """Generate a random UUID."""
69
70     return [ random.randint(0, 255) for _ in range(0, 16) ]
71
72 def uuidToString(u):
73     """Turn a numeric UUID to a hyphen-seperated one."""
74     return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
75                      "%02x" * 6]) % tuple(u)
76
77 MAX_MEMORY_TOTAL = 512
78 MAX_MEMORY_SINGLE = 256
79 MIN_MEMORY_SINGLE = 16
80 MAX_DISK_TOTAL = 50
81 MAX_DISK_SINGLE = 50
82 MIN_DISK_SINGLE = 0.1
83 MAX_VMS_TOTAL = 10
84 MAX_VMS_ACTIVE = 4
85
86 def getMachinesByOwner(owner):
87     """Return the machines owned by a given owner."""
88     return Machine.select_by(owner=owner)
89
90 def maxMemory(user, machine=None):
91     """Return the maximum memory for a machine or a user.
92
93     If machine is None, return the memory available for a new 
94     machine.  Else, return the maximum that machine can have.
95
96     on is a dictionary from machines to booleans, whether a machine is
97     on.  If None, it is recomputed. XXX make this global?
98     """
99
100     machines = getMachinesByOwner(user.username)
101     active_machines = [x for x in machines if g.uptimes[x]]
102     mem_usage = sum([x.memory for x in active_machines if x != machine])
103     return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
104
105 def maxDisk(user, machine=None):
106     machines = getMachinesByOwner(user.username)
107     disk_usage = sum([sum([y.size for y in x.disks])
108                       for x in machines if x != machine])
109     return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
110
111 def canAddVm(user):
112     machines = getMachinesByOwner(user.username)
113     active_machines = [x for x in machines if g.uptimes[x]]
114     return (len(machines) < MAX_VMS_TOTAL and
115             len(active_machines) < MAX_VMS_ACTIVE)
116
117 def haveAccess(user, machine):
118     """Return whether a user has access to a machine"""
119     if user.username == 'moo':
120         return True
121     return getafsgroups.checkLockerOwner(user.username,machine.owner)
122
123 def error(op, user, fields, err, emsg):
124     """Print an error page when a CodeError occurs"""
125     d = dict(op=op, user=user, errorMessage=str(err),
126              stderr=emsg)
127     return Template(file='error.tmpl', searchList=[d, global_dict]);
128
129 def invalidInput(op, user, fields, err, emsg):
130     """Print an error page when an InvalidInput exception occurs"""
131     d = dict(op=op, user=user, err_field=err.err_field,
132              err_value=str(err.err_value), stderr=emsg,
133              errorMessage=str(err))
134     return Template(file='invalid.tmpl', searchList=[d, global_dict]);
135
136 def validMachineName(name):
137     """Check that name is valid for a machine name"""
138     if not name:
139         return False
140     charset = string.ascii_letters + string.digits + '-_'
141     if name[0] in '-_' or len(name) > 22:
142         return False
143     for x in name:
144         if x not in charset:
145             return False
146     return True
147
148 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
149     """Kinit with a given username and keytab"""
150
151     p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
152                          stderr=subprocess.PIPE)
153     e = p.wait()
154     if e:
155         raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
156
157 def checkKinit():
158     """If we lack tickets, kinit."""
159     p = subprocess.Popen(['klist', '-s'])
160     if p.wait():
161         kinit()
162
163 def remctl(*args, **kws):
164     """Perform a remctl and return the output.
165
166     kinits if necessary, and outputs errors to stderr.
167     """
168     checkKinit()
169     p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
170                          + list(args),
171                          stdout=subprocess.PIPE,
172                          stderr=subprocess.PIPE)
173     if kws.get('err'):
174         p.wait()
175         return p.stdout.read(), p.stderr.read()
176     if p.wait():
177         raise CodeError('ERROR on remctl %s: %s' %
178                           (args, p.stderr.read()))
179     return p.stdout.read()
180
181 def lvcreate(machine, disk):
182     """Create a single disk for a machine"""
183     remctl('web', 'lvcreate', machine.name,
184            disk.guest_device_name, str(disk.size))
185     
186 def makeDisks(machine):
187     """Update the lvm partitions to add a disk."""
188     for disk in machine.disks:
189         lvcreate(machine, disk)
190
191 def bootMachine(machine, cdtype):
192     """Boot a machine with a given boot CD.
193
194     If cdtype is None, give no boot cd.  Otherwise, it is the string
195     id of the CD (e.g. 'gutsy_i386')
196     """
197     if cdtype is not None:
198         remctl('web', 'vmboot', machine.name,
199                cdtype)
200     else:
201         remctl('web', 'vmboot', machine.name)
202
203 def registerMachine(machine):
204     """Register a machine to be controlled by the web interface"""
205     remctl('web', 'register', machine.name)
206
207 def unregisterMachine(machine):
208     """Unregister a machine to not be controlled by the web interface"""
209     remctl('web', 'unregister', machine.name)
210
211 def parseStatus(s):
212     """Parse a status string into nested tuples of strings.
213
214     s = output of xm list --long <machine_name>
215     """
216     values = re.split('([()])', s)
217     stack = [[]]
218     for v in values[2:-2]: #remove initial and final '()'
219         if not v:
220             continue
221         v = v.strip()
222         if v == '(':
223             stack.append([])
224         elif v == ')':
225             if len(stack[-1]) == 1:
226                 stack[-1].append('')
227             stack[-2].append(stack[-1])
228             stack.pop()
229         else:
230             if not v:
231                 continue
232             stack[-1].extend(v.split())
233     return stack[-1]
234
235 def getUptimes(machines=None):
236     """Return a dictionary mapping machine names to uptime strings"""
237     value_string = remctl('web', 'listvms')
238     lines = value_string.splitlines()
239     d = {}
240     for line in lines:
241         lst = line.split()
242         name, id = lst[:2]
243         uptime = ' '.join(lst[2:])
244         d[name] = uptime
245     ans = {}
246     for m in machines:
247         ans[m] = d.get(m.name)
248     return ans
249
250 def statusInfo(machine):
251     """Return the status list for a given machine.
252
253     Gets and parses xm list --long
254     """
255     value_string, err_string = remctl('list-long', machine.name, err=True)
256     if 'Unknown command' in err_string:
257         raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
258     elif 'does not exist' in err_string:
259         return None
260     elif err_string:
261         raise CodeError("ERROR in remctl list-long %s:  %s" % (machine.name, err_string))
262     status = parseStatus(value_string)
263     return status
264
265 def hasVnc(status):
266     """Does the machine with a given status list support VNC?"""
267     if status is None:
268         return False
269     for l in status:
270         if l[0] == 'device' and l[1][0] == 'vfb':
271             d = dict(l[1][1:])
272             return 'location' in d
273     return False
274
275 def createVm(user, name, memory, disk, is_hvm, cdrom):
276     """Create a VM and put it in the database"""
277     # put stuff in the table
278     transaction = ctx.current.create_transaction()
279     try:
280         if memory > maxMemory(user):
281             raise InvalidInput('memory', memory,
282                                "Max %s" % maxMemory(user))
283         if disk > maxDisk(user) * 1024:
284             raise InvalidInput('disk', disk,
285                                "Max %s" % maxDisk(user))
286         if not canAddVm(user):
287             raise InvalidInput('create', True, 'Unable to create more VMs')
288         res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
289         id = res.fetchone()[0]
290         machine = Machine()
291         machine.machine_id = id
292         machine.name = name
293         machine.memory = memory
294         machine.owner = user.username
295         machine.contact = user.email
296         machine.uuid = uuidToString(randomUUID())
297         machine.boot_off_cd = True
298         machine_type = Type.get_by(hvm=is_hvm)
299         machine.type_id = machine_type.type_id
300         ctx.current.save(machine)
301         disk = Disk(machine.machine_id, 
302                     'hda', disk)
303         open = NIC.select_by(machine_id=None)
304         if not open: #No IPs left!
305             raise CodeError("No IP addresses left!  Contact sipb-xen-dev@mit.edu")
306         nic = open[0]
307         nic.machine_id = machine.machine_id
308         nic.hostname = name
309         ctx.current.save(nic)    
310         ctx.current.save(disk)
311         transaction.commit()
312     except:
313         transaction.rollback()
314         raise
315     registerMachine(machine)
316     makeDisks(machine)
317     # tell it to boot with cdrom
318     bootMachine(machine, cdrom)
319
320     return machine
321
322 def validMemory(user, memory, machine=None):
323     """Parse and validate limits for memory for a given user and machine."""
324     try:
325         memory = int(memory)
326         if memory < MIN_MEMORY_SINGLE:
327             raise ValueError
328     except ValueError:
329         raise InvalidInput('memory', memory, 
330                            "Minimum %s MB" % MIN_MEMORY_SINGLE)
331     if memory > maxMemory(user, machine):
332         raise InvalidInput('memory', memory,
333                            'Maximum %s MB' % maxMemory(user, machine))
334     return memory
335
336 def validDisk(user, disk, machine=None):
337     """Parse and validate limits for disk for a given user and machine."""
338     try:
339         disk = float(disk)
340         if disk > maxDisk(user, machine):
341             raise InvalidInput('disk', disk,
342                                "Maximum %s G" % maxDisk(user, machine))
343         disk = int(disk * 1024)
344         if disk < MIN_DISK_SINGLE * 1024:
345             raise ValueError
346     except ValueError:
347         raise InvalidInput('disk', disk,
348                            "Minimum %s GB" % MIN_DISK_SINGLE)
349     return disk
350
351 def create(user, fields):
352     """Handler for create requests."""
353     name = fields.getfirst('name')
354     if not validMachineName(name):
355         raise InvalidInput('name', name)
356     name = name.lower()
357
358     if Machine.get_by(name=name):
359         raise InvalidInput('name', name,
360                            "Already exists")
361     
362     memory = fields.getfirst('memory')
363     memory = validMemory(user, memory)
364     
365     disk = fields.getfirst('disk')
366     disk = validDisk(user, disk)
367
368     vm_type = fields.getfirst('vmtype')
369     if vm_type not in ('hvm', 'paravm'):
370         raise CodeError("Invalid vm type '%s'"  % vm_type)    
371     is_hvm = (vm_type == 'hvm')
372
373     cdrom = fields.getfirst('cdrom')
374     if cdrom is not None and not CDROM.get(cdrom):
375         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
376     
377     machine = createVm(user, name, memory, disk, is_hvm, cdrom)
378     d = dict(user=user,
379              machine=machine)
380     return Template(file='create.tmpl',
381                    searchList=[d, global_dict]);
382
383 def listVms(user, fields):
384     """Handler for list requests."""
385     machines = [m for m in Machine.select() if haveAccess(user, m)]    
386     on = {}
387     has_vnc = {}
388     on = g.uptimes
389     for m in machines:
390         if not on[m]:
391             has_vnc[m] = 'Off'
392         elif m.type.hvm:
393             has_vnc[m] = True
394         else:
395             has_vnc[m] = "ParaVM"+helppopup("paravm_console")
396     #     for m in machines:
397     #         status = statusInfo(m)
398     #         on[m.name] = status is not None
399     #         has_vnc[m.name] = hasVnc(status)
400     max_mem=maxMemory(user)
401     max_disk=maxDisk(user)
402     d = dict(user=user,
403              can_add_vm=canAddVm(user),
404              max_mem=max_mem,
405              max_disk=max_disk,
406              default_mem=max_mem,
407              default_disk=min(4.0, max_disk),
408              machines=machines,
409              has_vnc=has_vnc,
410              uptimes=g.uptimes,
411              cdroms=CDROM.select())
412     return Template(file='list.tmpl', searchList=[d, global_dict])
413
414 def testMachineId(user, machineId, exists=True):
415     """Parse, validate and check authorization for a given machineId.
416
417     If exists is False, don't check that it exists.
418     """
419     if machineId is None:
420         raise CodeError("No machine ID specified")
421     try:
422         machineId = int(machineId)
423     except ValueError:
424         raise CodeError("Invalid machine ID '%s'" % machineId)
425     machine = Machine.get(machineId)
426     if exists and machine is None:
427         raise CodeError("No such machine ID '%s'" % machineId)
428     if machine is not None and not haveAccess(user, machine):
429         raise CodeError("No access to machine ID '%s'" % machineId)
430     return machine
431
432 def vnc(user, fields):
433     """VNC applet page.
434
435     Note that due to same-domain restrictions, the applet connects to
436     the webserver, which needs to forward those requests to the xen
437     server.  The Xen server runs another proxy that (1) authenticates
438     and (2) finds the correct port for the VM.
439
440     You might want iptables like:
441
442     -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 
443     -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 
444     -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
445
446     Remember to enable iptables!
447     echo 1 > /proc/sys/net/ipv4/ip_forward
448     """
449     machine = testMachineId(user, fields.getfirst('machine_id'))
450     
451     TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
452
453     data = {}
454     data["user"] = user.username
455     data["machine"]=machine.name
456     data["expires"]=time.time()+(5*60)
457     pickledData = cPickle.dumps(data)
458     m = hmac.new(TOKEN_KEY, digestmod=sha)
459     m.update(pickledData)
460     token = {'data': pickledData, 'digest': m.digest()}
461     token = cPickle.dumps(token)
462     token = base64.urlsafe_b64encode(token)
463     
464     status = statusInfo(machine)
465     has_vnc = hasVnc(status)
466     
467     d = dict(user=user,
468              on=status,
469              has_vnc=has_vnc,
470              machine=machine,
471              hostname=os.environ.get('SERVER_NAME', 'localhost'),
472              authtoken=token)
473     return Template(file='vnc.tmpl',
474                    searchList=[d, global_dict])
475
476 def getNicInfo(data_dict, machine):
477     """Helper function for info, get data on nics for a machine.
478
479     Modifies data_dict to include the relevant data, and returns a list
480     of (key, name) pairs to display "name: data_dict[key]" to the user.
481     """
482     data_dict['num_nics'] = len(machine.nics)
483     nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
484                            ('nic%s_mac', 'NIC %s MAC Addr'),
485                            ('nic%s_ip', 'NIC %s IP'),
486                            ]
487     nic_fields = []
488     for i in range(len(machine.nics)):
489         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
490         data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
491         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
492         data_dict['nic%s_ip' % i] = machine.nics[i].ip
493     if len(machine.nics) == 1:
494         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
495     return nic_fields
496
497 def getDiskInfo(data_dict, machine):
498     """Helper function for info, get data on disks for a machine.
499
500     Modifies data_dict to include the relevant data, and returns a list
501     of (key, name) pairs to display "name: data_dict[key]" to the user.
502     """
503     data_dict['num_disks'] = len(machine.disks)
504     disk_fields_template = [('%s_size', '%s size')]
505     disk_fields = []
506     for disk in machine.disks:
507         name = disk.guest_device_name
508         disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
509         data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
510     return disk_fields
511
512 def deleteVM(machine):
513     """Delete a VM."""
514     try:
515         remctl('destroy', machine.name)
516     except:
517         pass
518     transaction = ctx.current.create_transaction()
519     delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
520     try:
521         for nic in machine.nics:
522             nic.machine_id = None
523             nic.hostname = None
524             ctx.current.save(nic)
525         for disk in machine.disks:
526             ctx.current.delete(disk)
527         ctx.current.delete(machine)
528         transaction.commit()
529     except:
530         transaction.rollback()
531         raise
532     for mname, dname in delete_disk_pairs:
533         remctl('web', 'lvremove', mname, dname)
534     unregisterMachine(machine)
535
536 def command(user, fields):
537     """Handler for running commands like boot and delete on a VM."""
538     print >> sys.stderr, time.time()-start_time
539     machine = testMachineId(user, fields.getfirst('machine_id'))
540     action = fields.getfirst('action')
541     cdrom = fields.getfirst('cdrom')
542     print >> sys.stderr, time.time()-start_time
543     if cdrom is not None and not CDROM.get(cdrom):
544         raise CodeError("Invalid cdrom type '%s'" % cdrom)    
545     if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
546         raise CodeError("Invalid action '%s'" % action)
547     if action == 'Reboot':
548         if cdrom is not None:
549             remctl('reboot', machine.name, cdrom)
550         else:
551             remctl('reboot', machine.name)
552     elif action == 'Power on':
553         if maxMemory(user) < machine.memory:
554             raise InvalidInput('action', 'Power on',
555                                "You don't have enough free RAM quota to turn on this machine")
556         bootMachine(machine, cdrom)
557     elif action == 'Power off':
558         remctl('destroy', machine.name)
559     elif action == 'Shutdown':
560         remctl('shutdown', machine.name)
561     elif action == 'Delete VM':
562         deleteVM(machine)
563     print >> sys.stderr, time.time()-start_time
564
565     d = dict(user=user,
566              command=action,
567              machine=machine)
568     return Template(file="command.tmpl", searchList=[d, global_dict])
569
570 def testOwner(user, owner, machine=None):
571     if not getafsgroups.checkLockerOwner(user.username, owner):
572         raise InvalidInput('owner', owner,
573                            "Invalid")
574     return owner
575
576 def testContact(user, contact, machine=None):
577     if contact != user.email:
578         raise InvalidInput('contact', contact,
579                            "Invalid")
580     return contact
581
582 def testDisk(user, disksize, machine=None):
583     return disksize
584
585 def testName(user, name, machine=None):
586     if Machine.select_by(name=name) == []:
587         return name
588     if name == machine.name:
589         return name
590     raise InvalidInput('name', name,
591                        "Already taken")
592
593 def testHostname(user, hostname, machine):
594     for nic in machine.nics:
595         if hostname == nic.hostname:
596             return hostname
597     # check if doesn't already exist
598     if NIC.select_by(hostname=hostname) == []:
599         return hostname
600     raise InvalidInput('hostname', hostname,
601                        "Different from before")
602
603
604 def modify(user, fields):
605     """Handler for modifying attributes of a machine."""
606     #XXX not written yet
607
608     transaction = ctx.current.create_transaction()
609     try:
610         machine = testMachineId(user, fields.getfirst('machine_id'))
611         owner = testOwner(user, fields.getfirst('owner'), machine)
612         contact = testContact(user, fields.getfirst('contact'))
613         hostname = testHostname(owner, fields.getfirst('hostname'),
614                             machine)
615         name = testName(user, fields.getfirst('name'), machine)
616         oldname = machine.name
617         command="modify"
618         olddisk = {}
619
620         memory = fields.getfirst('memory')
621         if memory is not None:
622             memory = validMemory(user, memory, machine)
623         else:
624             memory = machine.memory
625         if memory != machine.memory:
626             machine.memory = memory
627
628         disksize = testDisk(user, fields.getfirst('disk'))
629         if disksize is not None:
630             disksize = validDisk(user, disksize, machine)
631         else:
632             disksize = machine.disks[0].size
633         for disk in machine.disks:
634             olddisk[disk.guest_device_name] = disk.size
635             disk.size = disksize
636             ctx.current.save(disk)
637         
638         # XXX all NICs get same hostname on change?  Interface doesn't support more.
639         for nic in machine.nics:
640             nic.hostname = hostname
641             ctx.current.save(nic)
642
643         if owner != machine.owner:
644             machine.owner = owner
645         if name != machine.name:
646             machine.name = name
647             
648         ctx.current.save(machine)
649         transaction.commit()
650     except:
651         transaction.rollback()
652         raise
653     remctl("web", "moveregister", oldname, name)
654     for disk in machine.disks:
655         # XXX all disks get the same size on change?  Interface doesn't support more.
656         if disk.size != olddisk[disk.guest_device_name]:
657             remctl("web", "lvresize", oldname, disk.guest_device_name, str(disk.size))
658         if oldname != name:
659             remctl("web", "lvrename", oldname, disk.guest_device_name, name)
660     d = dict(user=user,
661              command=command,
662              machine=machine)
663     return Template(file="command.tmpl", searchList=[d, global_dict])    
664
665
666 def help(user, fields):
667     """Handler for help messages."""
668     simple = fields.getfirst('simple')
669     subjects = fields.getlist('subject')
670     
671     mapping = dict(paravm_console="""
672 ParaVM machines do not support console access over VNC.  To access
673 these machines, you either need to boot with a liveCD and ssh in or
674 hope that the sipb-xen maintainers add support for serial consoles.""",
675                    hvm_paravm="""
676 HVM machines use the virtualization features of the processor, while
677 ParaVM machines use Xen's emulation of virtualization features.  You
678 want an HVM virtualized machine.""",
679                    cpu_weight="""Don't ask us!  We're as mystified as you are.""",
680                    owner="""The Owner must be the name of a locker that you are an AFS
681 administrator of.  In particular, you or an AFS group you are a member
682 of must have AFS rlidwka bits on the locker.  You can check see who
683 administers the LOCKER locker using the command 'fs la /mit/LOCKER' on
684 Athena.)""")
685     
686     d = dict(user=user,
687              simple=simple,
688              subjects=subjects,
689              mapping=mapping)
690     
691     return Template(file="help.tmpl", searchList=[d, global_dict])
692     
693
694 def info(user, fields):
695     """Handler for info on a single VM."""
696     machine = testMachineId(user, fields.getfirst('machine_id'))
697     status = statusInfo(machine)
698     has_vnc = hasVnc(status)
699     if status is None:
700         main_status = dict(name=machine.name,
701                            memory=str(machine.memory))
702     else:
703         main_status = dict(status[1:])
704     start_time = float(main_status.get('start_time', 0))
705     uptime = datetime.timedelta(seconds=int(time.time()-start_time))
706     cpu_time_float = float(main_status.get('cpu_time', 0))
707     cputime = datetime.timedelta(seconds=int(cpu_time_float))
708     display_fields = """name uptime memory state cpu_weight on_reboot 
709      on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
710     display_fields = [('name', 'Name'),
711                       ('owner', 'Owner'),
712                       ('contact', 'Contact'),
713                       ('type', 'Type'),
714                       'NIC_INFO',
715                       ('uptime', 'uptime'),
716                       ('cputime', 'CPU usage'),
717                       ('memory', 'RAM'),
718                       'DISK_INFO',
719                       ('state', 'state (xen format)'),
720                       ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
721                       ('on_reboot', 'Action on VM reboot'),
722                       ('on_poweroff', 'Action on VM poweroff'),
723                       ('on_crash', 'Action on VM crash'),
724                       ('on_xend_start', 'Action on Xen start'),
725                       ('on_xend_stop', 'Action on Xen stop'),
726                       ('bootloader', 'Bootloader options'),
727                       ]
728     fields = []
729     machine_info = {}
730     machine_info['name'] = machine.name
731     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
732     machine_info['owner'] = machine.owner
733     machine_info['contact'] = machine.contact
734
735     nic_fields = getNicInfo(machine_info, machine)
736     nic_point = display_fields.index('NIC_INFO')
737     display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
738
739     disk_fields = getDiskInfo(machine_info, machine)
740     disk_point = display_fields.index('DISK_INFO')
741     display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
742     
743     main_status['memory'] += ' MB'
744     for field, disp in display_fields:
745         if field in ('uptime', 'cputime'):
746             fields.append((disp, locals()[field]))
747         elif field in machine_info:
748             fields.append((disp, machine_info[field]))
749         elif field in main_status:
750             fields.append((disp, main_status[field]))
751         else:
752             pass
753             #fields.append((disp, None))
754     max_mem = maxMemory(user, machine)
755     max_disk = maxDisk(user, machine)
756     d = dict(user=user,
757              cdroms=CDROM.select(),
758              on=status is not None,
759              machine=machine,
760              has_vnc=has_vnc,
761              uptime=str(uptime),
762              ram=machine.memory,
763              max_mem=max_mem,
764              max_disk=max_disk,
765              owner_help=helppopup("owner"),
766              fields = fields)
767     return Template(file='info.tmpl',
768                    searchList=[d, global_dict])
769
770 mapping = dict(list=listVms,
771                vnc=vnc,
772                command=command,
773                modify=modify,
774                info=info,
775                create=create,
776                help=help)
777
778 if __name__ == '__main__':
779     start_time = time.time()
780     fields = cgi.FieldStorage()
781     class User:
782         username = "moo"
783         email = 'moo@cow.com'
784     u = User()
785     g = Global(u)
786     if 'SSL_CLIENT_S_DN_Email' in os.environ:
787         username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
788         u.username = username
789         u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
790     else:
791         u.username = 'moo'
792         u.email = 'nobody'
793     connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
794     operation = os.environ.get('PATH_INFO', '')
795 #    print 'Content-Type: text/plain\n'
796 #    print operation
797     if not operation:
798         print "Status: 301 Moved Permanently"
799         print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
800         sys.exit(0)
801
802     if operation.startswith('/'):
803         operation = operation[1:]
804     if not operation:
805         operation = 'list'
806
807     def badOperation(u, e):
808         raise CodeError("Unknown operation")
809
810     fun = mapping.get(operation, badOperation)
811     if fun not in (help, ):
812         connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
813     try:
814         output = fun(u, fields)
815         print 'Content-Type: text/html\n'
816         sys.stderr.seek(0)
817         e = sys.stderr.read()
818         if e:
819             output = str(output)
820             output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
821         print output
822     except CodeError, err:
823         print 'Content-Type: text/html\n'
824         sys.stderr.seek(0)
825         e = sys.stderr.read()
826         sys.stderr=sys.stdout
827         print error(operation, u, fields, err, e)
828     except InvalidInput, err:
829         print 'Content-Type: text/html\n'
830         sys.stderr.seek(0)
831         e = sys.stderr.read()
832         sys.stderr=sys.stdout
833         print invalidInput(operation, u, fields, err, e)
834     except:
835         print 'Content-Type: text/plain\n'
836         sys.stderr.seek(0)
837         e = sys.stderr.read()
838         print e
839         print '----'
840         sys.stderr = sys.stdout
841         raise