ecb43633eecf5b7739ed49a90f3f47b04756bc52
[invirt/packages/invirt-web.git] / code / main.py
1 #!/usr/bin/python
2 """Main CGI script for web interface"""
3
4 from __future__ import with_statement
5
6 import base64
7 import cPickle
8 import cgi
9 import datetime
10 import hmac
11 import os
12 import random
13 import sha
14 import sys
15 import threading
16 import time
17 import urllib
18 import socket
19 import cherrypy
20 from cherrypy import _cperror
21 from StringIO import StringIO
22
23 def printError():
24     """Revert stderr to stdout, and print the contents of stderr"""
25     if isinstance(sys.stderr, StringIO):
26         print revertStandardError()
27
28 if __name__ == '__main__':
29     import atexit
30     atexit.register(printError)
31
32 import validation
33 import cache_acls
34 from webcommon import State
35 import controls
36 from getafsgroups import getAfsGroupMembers
37 from invirt import database
38 from invirt.database import Machine, CDROM, session, connect, MachineAccess, Type, Autoinstall
39 from invirt.config import structs as config
40 from invirt.common import InvalidInput, CodeError
41
42 from view import View, revertStandardError
43 import ajaxterm
44
45
46 static_dir = os.path.join(os.path.dirname(__file__), 'static')
47 InvirtStatic = cherrypy.tools.staticdir.handler(
48     root=static_dir,
49     dir=static_dir,
50     section='/static')
51
52 class InvirtUnauthWeb(View):
53     static = InvirtStatic
54
55     @cherrypy.expose
56     @cherrypy.tools.mako(filename="/unauth.mako")
57     def index(self):
58         return {'simple': True}
59
60 class InvirtWeb(View):
61     def __init__(self):
62         super(self.__class__,self).__init__()
63         connect()
64         self._cp_config['tools.require_login.on'] = True
65         self._cp_config['tools.catch_stderr.on'] = True
66         self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
67                                                  'from invirt import database']
68         self._cp_config['request.error_response'] = self.handle_error
69
70     static = InvirtStatic
71
72     @cherrypy.expose
73     @cherrypy.tools.mako(filename="/invalid.mako")
74     def invalidInput(self):
75         """Print an error page when an InvalidInput exception occurs"""
76         err = cherrypy.request.prev.params["err"]
77         emsg = cherrypy.request.prev.params["emsg"]
78         d = dict(err_field=err.err_field,
79                  err_value=str(err.err_value), stderr=emsg,
80                  errorMessage=str(err))
81         return d
82
83     @cherrypy.expose
84     @cherrypy.tools.mako(filename="/error.mako")
85     def error(self):
86         """Print an error page when an exception occurs"""
87         op = cherrypy.request.prev.path_info
88         username = cherrypy.request.login
89         err = cherrypy.request.prev.params["err"]
90         emsg = cherrypy.request.prev.params["emsg"]
91         traceback = cherrypy.request.prev.params["traceback"]
92         d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
93                  errorMessage=str(err), stderr=emsg, traceback=traceback)
94         error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
95         details = error_raw.render(**d)
96         exclude = config.web.errormail_exclude
97         if username not in exclude and '*' not in exclude:
98             send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
99                             details)
100         d['details'] = details
101         return d
102
103     def __getattr__(self, name):
104         if name in ("admin", "overlord"):
105             if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
106                 raise InvalidInput('username', cherrypy.request.login,
107                                    'Not in admin group %s.' % config.adminacl)
108             cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
109             return self
110         else:
111             return super(InvirtWeb, self).__getattr__(name)
112
113     def handle_error(self):
114         err = sys.exc_info()[1]
115         if isinstance(err, InvalidInput):
116             cherrypy.request.params['err'] = err
117             cherrypy.request.params['emsg'] = revertStandardError()
118             raise cherrypy.InternalRedirect('/invalidInput')
119         if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
120             cherrypy.request.params['err'] = err
121             cherrypy.request.params['emsg'] = revertStandardError()
122             cherrypy.request.params['traceback'] = _cperror.format_exc()
123             raise cherrypy.InternalRedirect('/error')
124         # fall back to cherrypy default error page
125         cherrypy.HTTPError(500).set_response()
126
127     @cherrypy.expose
128     @cherrypy.tools.mako(filename="/list.mako")
129     def list(self, result=None):
130         """Handler for list requests."""
131         d = getListDict(cherrypy.request.login, cherrypy.request.state)
132         if result is not None:
133             d['result'] = result
134         return d
135     index=list
136
137     @cherrypy.expose
138     @cherrypy.tools.mako(filename="/help.mako")
139     def help(self, subject=None, simple=False):
140         """Handler for help messages."""
141
142         help_mapping = {
143             'Autoinstalls': """
144 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
145 ParaVM.  You can access the resulting system by logging into the <a
146 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
147 with your Kerberos tickets; there is no root password so sshd will
148 refuse login.</p>
149
150 <p>Under the covers, the autoinstaller uses our own patched version of
151 xen-create-image, which is a tool based on debootstrap.  If you log
152 into the serial console while the install is running, you can watch
153 it.
154 """,
155             'ParaVM Console': """
156 ParaVM machines do not support local console access over VNC.  To
157 access the serial console of these machines, you can SSH with Kerberos
158 to %s, using the name of the machine as your
159 username.""" % config.console.hostname,
160             'HVM/ParaVM': """
161 HVM machines use the virtualization features of the processor, while
162 ParaVM machines rely on a modified kernel to communicate directly with
163 the hypervisor.  HVMs support boot CDs of any operating system, and
164 the VNC console applet.  The three-minute autoinstaller produces
165 ParaVMs.  ParaVMs typically are more efficient, and always support the
166 <a href="help?subject=ParaVM+Console">console server</a>.</p>
167
168 <p>More details are <a
169 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
170 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
171 (which you can skip by using the autoinstaller to begin with.)</p>
172
173 <p>We recommend using a ParaVM when possible and an HVM when necessary.
174 """,
175             'CPU Weight': """
176 Don't ask us!  We're as mystified as you are.""",
177             'Owner': """
178 The owner field is used to determine <a
179 href="help?subject=Quotas">quotas</a>.  It must be the name of a
180 locker that you are an AFS administrator of.  In particular, you or an
181 AFS group you are a member of must have AFS rlidwka bits on the
182 locker.  You can check who administers the LOCKER locker using the
183 commands 'attach LOCKER; fs la /mit/LOCKER' on Athena.)  See also <a
184 href="help?subject=Administrator">administrator</a>.""",
185             'Administrator': """
186 The administrator field determines who can access the console and
187 power on and off the machine.  This can be either a user or a moira
188 group.""",
189             'Quotas': """
190 Quotas are determined on a per-locker basis.  Each locker may have a
191 maximum of 512 mebibytes of active ram, 50 gibibytes of disk, and 4
192 active machines.""",
193             'Console': """
194 <strong>Framebuffer:</strong> At a Linux boot prompt in your VM, try
195 setting <tt>fb=false</tt> to disable the framebuffer.  If you don't,
196 your machine will run just fine, but the applet's display of the
197 console will suffer artifacts.
198 """,
199             'Windows': """
200 <strong>Windows Vista:</strong> The Vista image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-email.txt">the licensing confirmation e-mail</a> for details. The installer requires 512 MiB RAM and at least 7.5 GiB disk space (15 GiB or more recommended).<br>
201 <strong>Windows XP:</strong> This is the volume license CD image. You will need your own volume license key to complete the install. We do not have these available for the general MIT community; ask your department if they have one, or visit <a href="http://msca.mit.edu/">http://msca.mit.edu/</a> if you are staff/faculty to request one.
202 """
203             }
204
205         if not subject:
206             subject = sorted(help_mapping.keys())
207         if not isinstance(subject, list):
208             subject = [subject]
209
210         return dict(simple=simple,
211                     subjects=subject,
212                     mapping=help_mapping)
213     help._cp_config['tools.require_login.on'] = False
214
215     def parseCreate(self, fields):
216         kws = dict([(kw, fields[kw]) for kw in
217          'name description owner memory disksize vmtype cdrom autoinstall'.split()
218                     if fields[kw]])
219         validate = validation.Validate(cherrypy.request.login,
220                                        cherrypy.request.state,
221                                        strict=True, **kws)
222         return dict(contact=cherrypy.request.login, name=validate.name,
223                     description=validate.description, memory=validate.memory,
224                     disksize=validate.disksize, owner=validate.owner,
225                     machine_type=getattr(validate, 'vmtype', Defaults.type),
226                     cdrom=getattr(validate, 'cdrom', None),
227                     autoinstall=getattr(validate, 'autoinstall', None))
228
229     @cherrypy.expose
230     @cherrypy.tools.mako(filename="/list.mako")
231     @cherrypy.tools.require_POST()
232     def create(self, **fields):
233         """Handler for create requests."""
234         try:
235             parsed_fields = self.parseCreate(fields)
236             machine = controls.createVm(cherrypy.request.login,
237                                         cherrypy.request.state, **parsed_fields)
238         except InvalidInput, err:
239             pass
240         else:
241             err = None
242         cherrypy.request.state.clear() #Changed global state
243         d = getListDict(cherrypy.request.login, cherrypy.request.state)
244         d['err'] = err
245         if err:
246             for field, value in fields.items():
247                 setattr(d['defaults'], field, value)
248         else:
249             d['new_machine'] = parsed_fields['name']
250         return d
251
252     @cherrypy.expose
253     @cherrypy.tools.mako(filename="/helloworld.mako")
254     def helloworld(self, **kwargs):
255         return {'request': cherrypy.request, 'kwargs': kwargs}
256     helloworld._cp_config['tools.require_login.on'] = False
257
258     @cherrypy.expose
259     def errortest(self):
260         """Throw an error, to test the error-tracing mechanisms."""
261         print >>sys.stderr, "look ma, it's a stderr"
262         raise RuntimeError("test of the emergency broadcast system")
263
264     class MachineView(View):
265         def __getattr__(self, name):
266             """Synthesize attributes to allow RESTful URLs like
267             /machine/13/info. This is hairy. CherryPy 3.2 adds a
268             method called _cp_dispatch that allows you to explicitly
269             handle URLs that can't be mapped, and it allows you to
270             rewrite the path components and continue processing.
271
272             This function gets the next path component being resolved
273             as a string. _cp_dispatch will get an array of strings
274             representing any subsequent path components as well."""
275
276             try:
277                 cherrypy.request.params['machine_id'] = int(name)
278                 return self
279             except ValueError:
280                 return None
281
282         @cherrypy.expose
283         @cherrypy.tools.mako(filename="/info.mako")
284         def info(self, machine_id):
285             """Handler for info on a single VM."""
286             machine = validation.Validate(cherrypy.request.login,
287                                           cherrypy.request.state,
288                                           machine_id=machine_id).machine
289             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
290             return d
291         index = info
292
293         @cherrypy.expose
294         @cherrypy.tools.mako(filename="/info.mako")
295         @cherrypy.tools.require_POST()
296         def modify(self, machine_id, **fields):
297             """Handler for modifying attributes of a machine."""
298             try:
299                 modify_dict = modifyDict(cherrypy.request.login,
300                                          cherrypy.request.state,
301                                          machine_id, fields)
302             except InvalidInput, err:
303                 result = None
304                 machine = validation.Validate(cherrypy.request.login,
305                                               cherrypy.request.state,
306                                               machine_id=machine_id).machine
307             else:
308                 machine = modify_dict['machine']
309                 result = 'Success!'
310                 err = None
311             info_dict = infoDict(cherrypy.request.login,
312                                  cherrypy.request.state, machine)
313             info_dict['err'] = err
314             if err:
315                 for field, value in fields.items():
316                     setattr(info_dict['defaults'], field, value)
317             info_dict['result'] = result
318             return info_dict
319
320         @cherrypy.expose
321         @cherrypy.tools.mako(filename="/vnc.mako")
322         def vnc(self, machine_id):
323             """VNC applet page.
324
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.
329
330             You might want iptables like:
331
332             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
333             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
334             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
335             --dport 10003 -j SNAT --to-source 18.187.7.142
336             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
337             --dport 10003 -j ACCEPT
338
339             Remember to enable iptables!
340             echo 1 > /proc/sys/net/ipv4/ip_forward
341             """
342             machine = validation.Validate(cherrypy.request.login,
343                                           cherrypy.request.state,
344                                           machine_id=machine_id).machine
345             token = controls.vnctoken(machine)
346             host = controls.listHost(machine)
347             if host:
348                 port = 10003 + [h.hostname for h in config.hosts].index(host)
349             else:
350                 port = 5900 # dummy
351
352             status = controls.statusInfo(machine)
353             has_vnc = hasVnc(status)
354
355             d = dict(on=status,
356                      has_vnc=has_vnc,
357                      machine=machine,
358                      hostname=cherrypy.request.local.name,
359                      port=port,
360                      authtoken=token)
361             return d
362
363         @cherrypy.expose
364         @cherrypy.tools.mako(filename="/command.mako")
365         @cherrypy.tools.require_POST()
366         def command(self, command_name, machine_id, **kwargs):
367             """Handler for running commands like boot and delete on a VM."""
368             back = kwargs.get('back')
369             if command_name == 'delete':
370                 back = 'list'
371             try:
372                 d = controls.commandResult(cherrypy.request.login,
373                                            cherrypy.request.state,
374                                            command_name, machine_id, kwargs)
375             except InvalidInput, err:
376                 if not back:
377                     raise
378                 print >> sys.stderr, err
379                 result = str(err)
380             else:
381                 result = 'Success!'
382                 if not back:
383                     return d
384             if back == 'list':
385                 cherrypy.request.state.clear() #Changed global state
386                 raise cherrypy.InternalRedirect('/list?result=%s'
387                                                 % urllib.quote(result))
388             elif back == 'info':
389                 raise cherrypy.HTTPRedirect(cherrypy.request.base
390                                             + '/machine/%d/' % machine_id,
391                                             status=303)
392             else:
393                 raise InvalidInput('back', back, 'Not a known back page.')
394
395         atmulti = ajaxterm.Multiplex()
396         atsessions = {}
397         atsessions_lock = threading.Lock()
398
399         @cherrypy.expose
400         @cherrypy.tools.mako(filename="/terminal.mako")
401         def terminal(self, machine_id):
402             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
403
404             status = controls.statusInfo(machine)
405             has_vnc = hasVnc(status)
406
407             d = dict(on=status,
408                      has_vnc=has_vnc,
409                      machine=machine,
410                      hostname=cherrypy.request.local.name)
411             return d
412
413         @cherrypy.expose
414         @cherrypy.tools.gzip()
415         def at(self, machine_id, k=None, c=0, force=0):
416             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
417             with self.atsessions_lock:
418                 if machine_id in self.atsessions:
419                     term = self.atsessions[machine_id]
420                 else:
421                     print >>sys.stderr, "spawning new session for terminal to ",machine_id
422                     term = self.atmulti.create(
423                         ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
424                         )
425                     # Clear out old sessions when fd is reused
426                     for key in self.atsessions:
427                         if self.atsessions[key] == term:
428                             del self.atsessions[key]
429                     self.atsessions[machine_id] = term
430                 if k:
431                     self.atmulti.proc_write(term,k)
432                 time.sleep(0.002)
433                 dump=self.atmulti.dump(term,c,int(force))
434                 cherrypy.response.headers['Content-Type']='text/xml'
435                 if isinstance(dump,str):
436                     return dump
437                 else:
438                     print "Removing session for", machine_id
439                     del self.atsessions[machine_id]
440                     return '<?xml version="1.0"?><idem></idem>'
441
442     machine = MachineView()
443
444
445 class Defaults:
446     """Class to store default values for fields."""
447     memory = 256
448     disk = 4.0
449     cdrom = ''
450     autoinstall = ''
451     name = ''
452     description = ''
453     administrator = ''
454     type = 'linux-hvm'
455
456     def __init__(self, max_memory=None, max_disk=None, **kws):
457         if max_memory is not None:
458             self.memory = min(self.memory, max_memory)
459         if max_disk is not None:
460             self.disk = min(self.disk, max_disk)
461         for key in kws:
462             setattr(self, key, kws[key])
463
464 def hasVnc(status):
465     """Does the machine with a given status list support VNC?"""
466     if status is None:
467         return False
468     for l in status:
469         if l[0] == 'device' and l[1][0] == 'vfb':
470             d = dict(l[1][1:])
471             return 'location' in d
472     return False
473
474
475 def getListDict(username, state):
476     """Gets the list of local variables used by list.tmpl."""
477     machines = state.machines
478     on = {}
479     has_vnc = {}
480     installing = {}
481     xmlist = state.xmlist
482     for m in machines:
483         if m not in xmlist:
484             has_vnc[m] = 'Off'
485             m.uptime = None
486         else:
487             m.uptime = xmlist[m]['uptime']
488             installing[m] = bool(xmlist[m].get('autoinstall'))
489             if xmlist[m]['console']:
490                 has_vnc[m] = True
491             elif m.type.hvm:
492                 has_vnc[m] = "WTF?"
493             else:
494                 has_vnc[m] = "ParaVM"
495     max_memory = validation.maxMemory(username, state)
496     max_disk = validation.maxDisk(username)
497     defaults = Defaults(max_memory=max_memory,
498                         max_disk=max_disk,
499                         owner=username)
500     def sortkey(machine):
501         return (machine.owner != username, machine.owner, machine.name)
502     machines = sorted(machines, key=sortkey)
503     d = dict(user=username,
504              cant_add_vm=validation.cantAddVm(username, state),
505              max_memory=max_memory,
506              max_disk=max_disk,
507              defaults=defaults,
508              machines=machines,
509              has_vnc=has_vnc,
510              installing=installing)
511     return d
512
513 def getHostname(nic):
514     """Find the hostname associated with a NIC.
515
516     XXX this should be merged with the similar logic in DNS and DHCP.
517     """
518     if nic.hostname:
519         hostname = nic.hostname
520     elif nic.machine:
521         hostname = nic.machine.name
522     else:
523         return None
524     if '.' in hostname:
525         return hostname
526     else:
527         return hostname + '.' + config.dns.domains[0]
528
529 def getNicInfo(data_dict, machine):
530     """Helper function for info, get data on nics for a machine.
531
532     Modifies data_dict to include the relevant data, and returns a list
533     of (key, name) pairs to display "name: data_dict[key]" to the user.
534     """
535     data_dict['num_nics'] = len(machine.nics)
536     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
537                            ('nic%s_mac', 'NIC %s MAC Addr'),
538                            ('nic%s_ip', 'NIC %s IP'),
539                            ]
540     nic_fields = []
541     for i in range(len(machine.nics)):
542         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
543         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
544         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
545         data_dict['nic%s_ip' % i] = machine.nics[i].ip
546     if len(machine.nics) == 1:
547         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
548     return nic_fields
549
550 def getDiskInfo(data_dict, machine):
551     """Helper function for info, get data on disks for a machine.
552
553     Modifies data_dict to include the relevant data, and returns a list
554     of (key, name) pairs to display "name: data_dict[key]" to the user.
555     """
556     data_dict['num_disks'] = len(machine.disks)
557     disk_fields_template = [('%s_size', '%s size')]
558     disk_fields = []
559     for disk in machine.disks:
560         name = disk.guest_device_name
561         disk_fields.extend([(x % name, y % name) for x, y in
562                             disk_fields_template])
563         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
564     return disk_fields
565
566 def modifyDict(username, state, machine_id, fields):
567     """Modify a machine as specified by CGI arguments.
568
569     Return a dict containing the machine that was modified.
570     """
571     olddisk = {}
572     session.begin()
573     try:
574         kws = dict([(kw, fields[kw]) for kw in
575          'owner admin contact name description memory vmtype disksize'.split()
576                     if fields[kw]])
577         kws['machine_id'] = machine_id
578         validate = validation.Validate(username, state, **kws)
579         machine = validate.machine
580         oldname = machine.name
581
582         if hasattr(validate, 'memory'):
583             machine.memory = validate.memory
584
585         if hasattr(validate, 'vmtype'):
586             machine.type = validate.vmtype
587
588         if hasattr(validate, 'disksize'):
589             disksize = validate.disksize
590             disk = machine.disks[0]
591             if disk.size != disksize:
592                 olddisk[disk.guest_device_name] = disksize
593                 disk.size = disksize
594                 session.save_or_update(disk)
595
596         update_acl = False
597         if hasattr(validate, 'owner') and validate.owner != machine.owner:
598             machine.owner = validate.owner
599             update_acl = True
600         if hasattr(validate, 'name'):
601             machine.name = validate.name
602             for n in machine.nics:
603                 if n.hostname == oldname:
604                     n.hostname = validate.name
605         if hasattr(validate, 'description'):
606             machine.description = validate.description
607         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
608             machine.administrator = validate.admin
609             update_acl = True
610         if hasattr(validate, 'contact'):
611             machine.contact = validate.contact
612
613         session.save_or_update(machine)
614         if update_acl:
615             cache_acls.refreshMachine(machine)
616         session.commit()
617     except:
618         session.rollback()
619         raise
620     for diskname in olddisk:
621         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
622     if hasattr(validate, 'name'):
623         controls.renameMachine(machine, oldname, validate.name)
624     return dict(machine=machine)
625
626 def infoDict(username, state, machine):
627     """Get the variables used by info.tmpl."""
628     status = controls.statusInfo(machine)
629     has_vnc = hasVnc(status)
630     if status is None:
631         main_status = dict(name=machine.name,
632                            memory=str(machine.memory))
633         uptime = None
634         cputime = None
635     else:
636         main_status = dict(status[1:])
637         main_status['host'] = controls.listHost(machine)
638         start_time = float(main_status.get('start_time', 0))
639         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
640         cpu_time_float = float(main_status.get('cpu_time', 0))
641         cputime = datetime.timedelta(seconds=int(cpu_time_float))
642     display_fields = [('name', 'Name'),
643                       ('description', 'Description'),
644                       ('owner', 'Owner'),
645                       ('administrator', 'Administrator'),
646                       ('contact', 'Contact'),
647                       ('type', 'Type'),
648                       'NIC_INFO',
649                       ('uptime', 'uptime'),
650                       ('cputime', 'CPU usage'),
651                       ('host', 'Hosted on'),
652                       ('memory', 'RAM'),
653                       'DISK_INFO',
654                       ('state', 'state (xen format)'),
655                       ]
656     fields = []
657     machine_info = {}
658     machine_info['name'] = machine.name
659     machine_info['description'] = machine.description
660     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
661     machine_info['owner'] = machine.owner
662     machine_info['administrator'] = machine.administrator
663     machine_info['contact'] = machine.contact
664
665     nic_fields = getNicInfo(machine_info, machine)
666     nic_point = display_fields.index('NIC_INFO')
667     display_fields = (display_fields[:nic_point] + nic_fields +
668                       display_fields[nic_point+1:])
669
670     disk_fields = getDiskInfo(machine_info, machine)
671     disk_point = display_fields.index('DISK_INFO')
672     display_fields = (display_fields[:disk_point] + disk_fields +
673                       display_fields[disk_point+1:])
674
675     main_status['memory'] += ' MiB'
676     for field, disp in display_fields:
677         if field in ('uptime', 'cputime') and locals()[field] is not None:
678             fields.append((disp, locals()[field]))
679         elif field in machine_info:
680             fields.append((disp, machine_info[field]))
681         elif field in main_status:
682             fields.append((disp, main_status[field]))
683         else:
684             pass
685             #fields.append((disp, None))
686
687     max_mem = validation.maxMemory(machine.owner, state, machine, False)
688     max_disk = validation.maxDisk(machine.owner, machine)
689     defaults = Defaults()
690     for name in 'machine_id name description administrator owner memory contact'.split():
691         if getattr(machine, name):
692             setattr(defaults, name, getattr(machine, name))
693     defaults.type = machine.type.type_id
694     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
695     d = dict(user=username,
696              on=status is not None,
697              machine=machine,
698              defaults=defaults,
699              has_vnc=has_vnc,
700              uptime=str(uptime),
701              ram=machine.memory,
702              max_mem=max_mem,
703              max_disk=max_disk,
704              fields = fields)
705     return d
706
707 def send_error_mail(subject, body):
708     import subprocess
709
710     to = config.web.errormail
711     mail = """To: %s
712 From: root@%s
713 Subject: %s
714
715 %s
716 """ % (to, config.web.hostname, subject, body)
717     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
718                          stdin=subprocess.PIPE)
719     p.stdin.write(mail)
720     p.stdin.close()
721     p.wait()
722
723 random.seed() #sigh