disable GETting to post keyboards to ajaxterm
[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.require_POST()
415         @cherrypy.tools.gzip()
416         def at(self, machine_id, k=None, c=0, force=0):
417             machine = validation.Validate(cherrypy.request.login, cherrypy.request.state, machine_id=machine_id).machine
418             with self.atsessions_lock:
419                 if machine_id in self.atsessions:
420                     term = self.atsessions[machine_id]
421                 else:
422                     print >>sys.stderr, "spawning new session for terminal to ",machine_id
423                     term = self.atmulti.create(
424                         ["ssh", "-e","none", "-l", machine.name, config.console.hostname]
425                         )
426                     # Clear out old sessions when fd is reused
427                     for key in self.atsessions:
428                         if self.atsessions[key] == term:
429                             del self.atsessions[key]
430                     self.atsessions[machine_id] = term
431                 if k:
432                     self.atmulti.proc_write(term,k)
433                 time.sleep(0.002)
434                 dump=self.atmulti.dump(term,c,int(force))
435                 cherrypy.response.headers['Content-Type']='text/xml'
436                 if isinstance(dump,str):
437                     return dump
438                 else:
439                     print "Removing session for", machine_id
440                     del self.atsessions[machine_id]
441                     return '<?xml version="1.0"?><idem></idem>'
442
443     machine = MachineView()
444
445
446 class Defaults:
447     """Class to store default values for fields."""
448     memory = 256
449     disk = 4.0
450     cdrom = ''
451     autoinstall = ''
452     name = ''
453     description = ''
454     administrator = ''
455     type = 'linux-hvm'
456
457     def __init__(self, max_memory=None, max_disk=None, **kws):
458         if max_memory is not None:
459             self.memory = min(self.memory, max_memory)
460         if max_disk is not None:
461             self.disk = min(self.disk, max_disk)
462         for key in kws:
463             setattr(self, key, kws[key])
464
465 def hasVnc(status):
466     """Does the machine with a given status list support VNC?"""
467     if status is None:
468         return False
469     for l in status:
470         if l[0] == 'device' and l[1][0] == 'vfb':
471             d = dict(l[1][1:])
472             return 'location' in d
473     return False
474
475
476 def getListDict(username, state):
477     """Gets the list of local variables used by list.tmpl."""
478     machines = state.machines
479     on = {}
480     has_vnc = {}
481     installing = {}
482     xmlist = state.xmlist
483     for m in machines:
484         if m not in xmlist:
485             has_vnc[m] = 'Off'
486             m.uptime = None
487         else:
488             m.uptime = xmlist[m]['uptime']
489             installing[m] = bool(xmlist[m].get('autoinstall'))
490             if xmlist[m]['console']:
491                 has_vnc[m] = True
492             elif m.type.hvm:
493                 has_vnc[m] = "WTF?"
494             else:
495                 has_vnc[m] = "ParaVM"
496     max_memory = validation.maxMemory(username, state)
497     max_disk = validation.maxDisk(username)
498     defaults = Defaults(max_memory=max_memory,
499                         max_disk=max_disk,
500                         owner=username)
501     def sortkey(machine):
502         return (machine.owner != username, machine.owner, machine.name)
503     machines = sorted(machines, key=sortkey)
504     d = dict(user=username,
505              cant_add_vm=validation.cantAddVm(username, state),
506              max_memory=max_memory,
507              max_disk=max_disk,
508              defaults=defaults,
509              machines=machines,
510              has_vnc=has_vnc,
511              installing=installing)
512     return d
513
514 def getHostname(nic):
515     """Find the hostname associated with a NIC.
516
517     XXX this should be merged with the similar logic in DNS and DHCP.
518     """
519     if nic.hostname:
520         hostname = nic.hostname
521     elif nic.machine:
522         hostname = nic.machine.name
523     else:
524         return None
525     if '.' in hostname:
526         return hostname
527     else:
528         return hostname + '.' + config.dns.domains[0]
529
530 def getNicInfo(data_dict, machine):
531     """Helper function for info, get data on nics for a machine.
532
533     Modifies data_dict to include the relevant data, and returns a list
534     of (key, name) pairs to display "name: data_dict[key]" to the user.
535     """
536     data_dict['num_nics'] = len(machine.nics)
537     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
538                            ('nic%s_mac', 'NIC %s MAC Addr'),
539                            ('nic%s_ip', 'NIC %s IP'),
540                            ]
541     nic_fields = []
542     for i in range(len(machine.nics)):
543         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
544         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
545         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
546         data_dict['nic%s_ip' % i] = machine.nics[i].ip
547     if len(machine.nics) == 1:
548         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
549     return nic_fields
550
551 def getDiskInfo(data_dict, machine):
552     """Helper function for info, get data on disks for a machine.
553
554     Modifies data_dict to include the relevant data, and returns a list
555     of (key, name) pairs to display "name: data_dict[key]" to the user.
556     """
557     data_dict['num_disks'] = len(machine.disks)
558     disk_fields_template = [('%s_size', '%s size')]
559     disk_fields = []
560     for disk in machine.disks:
561         name = disk.guest_device_name
562         disk_fields.extend([(x % name, y % name) for x, y in
563                             disk_fields_template])
564         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
565     return disk_fields
566
567 def modifyDict(username, state, machine_id, fields):
568     """Modify a machine as specified by CGI arguments.
569
570     Return a dict containing the machine that was modified.
571     """
572     olddisk = {}
573     session.begin()
574     try:
575         kws = dict([(kw, fields[kw]) for kw in
576          'owner admin contact name description memory vmtype disksize'.split()
577                     if fields[kw]])
578         kws['machine_id'] = machine_id
579         validate = validation.Validate(username, state, **kws)
580         machine = validate.machine
581         oldname = machine.name
582
583         if hasattr(validate, 'memory'):
584             machine.memory = validate.memory
585
586         if hasattr(validate, 'vmtype'):
587             machine.type = validate.vmtype
588
589         if hasattr(validate, 'disksize'):
590             disksize = validate.disksize
591             disk = machine.disks[0]
592             if disk.size != disksize:
593                 olddisk[disk.guest_device_name] = disksize
594                 disk.size = disksize
595                 session.save_or_update(disk)
596
597         update_acl = False
598         if hasattr(validate, 'owner') and validate.owner != machine.owner:
599             machine.owner = validate.owner
600             update_acl = True
601         if hasattr(validate, 'name'):
602             machine.name = validate.name
603             for n in machine.nics:
604                 if n.hostname == oldname:
605                     n.hostname = validate.name
606         if hasattr(validate, 'description'):
607             machine.description = validate.description
608         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
609             machine.administrator = validate.admin
610             update_acl = True
611         if hasattr(validate, 'contact'):
612             machine.contact = validate.contact
613
614         session.save_or_update(machine)
615         if update_acl:
616             cache_acls.refreshMachine(machine)
617         session.commit()
618     except:
619         session.rollback()
620         raise
621     for diskname in olddisk:
622         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
623     if hasattr(validate, 'name'):
624         controls.renameMachine(machine, oldname, validate.name)
625     return dict(machine=machine)
626
627 def infoDict(username, state, machine):
628     """Get the variables used by info.tmpl."""
629     status = controls.statusInfo(machine)
630     has_vnc = hasVnc(status)
631     if status is None:
632         main_status = dict(name=machine.name,
633                            memory=str(machine.memory))
634         uptime = None
635         cputime = None
636     else:
637         main_status = dict(status[1:])
638         main_status['host'] = controls.listHost(machine)
639         start_time = float(main_status.get('start_time', 0))
640         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
641         cpu_time_float = float(main_status.get('cpu_time', 0))
642         cputime = datetime.timedelta(seconds=int(cpu_time_float))
643     display_fields = [('name', 'Name'),
644                       ('description', 'Description'),
645                       ('owner', 'Owner'),
646                       ('administrator', 'Administrator'),
647                       ('contact', 'Contact'),
648                       ('type', 'Type'),
649                       'NIC_INFO',
650                       ('uptime', 'uptime'),
651                       ('cputime', 'CPU usage'),
652                       ('host', 'Hosted on'),
653                       ('memory', 'RAM'),
654                       'DISK_INFO',
655                       ('state', 'state (xen format)'),
656                       ]
657     fields = []
658     machine_info = {}
659     machine_info['name'] = machine.name
660     machine_info['description'] = machine.description
661     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
662     machine_info['owner'] = machine.owner
663     machine_info['administrator'] = machine.administrator
664     machine_info['contact'] = machine.contact
665
666     nic_fields = getNicInfo(machine_info, machine)
667     nic_point = display_fields.index('NIC_INFO')
668     display_fields = (display_fields[:nic_point] + nic_fields +
669                       display_fields[nic_point+1:])
670
671     disk_fields = getDiskInfo(machine_info, machine)
672     disk_point = display_fields.index('DISK_INFO')
673     display_fields = (display_fields[:disk_point] + disk_fields +
674                       display_fields[disk_point+1:])
675
676     main_status['memory'] += ' MiB'
677     for field, disp in display_fields:
678         if field in ('uptime', 'cputime') and locals()[field] is not None:
679             fields.append((disp, locals()[field]))
680         elif field in machine_info:
681             fields.append((disp, machine_info[field]))
682         elif field in main_status:
683             fields.append((disp, main_status[field]))
684         else:
685             pass
686             #fields.append((disp, None))
687
688     max_mem = validation.maxMemory(machine.owner, state, machine, False)
689     max_disk = validation.maxDisk(machine.owner, machine)
690     defaults = Defaults()
691     for name in 'machine_id name description administrator owner memory contact'.split():
692         if getattr(machine, name):
693             setattr(defaults, name, getattr(machine, name))
694     defaults.type = machine.type.type_id
695     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
696     d = dict(user=username,
697              on=status is not None,
698              machine=machine,
699              defaults=defaults,
700              has_vnc=has_vnc,
701              uptime=str(uptime),
702              ram=machine.memory,
703              max_mem=max_mem,
704              max_disk=max_disk,
705              fields = fields)
706     return d
707
708 def send_error_mail(subject, body):
709     import subprocess
710
711     to = config.web.errormail
712     mail = """To: %s
713 From: root@%s
714 Subject: %s
715
716 %s
717 """ % (to, config.web.hostname, subject, body)
718     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
719                          stdin=subprocess.PIPE)
720     p.stdin.write(mail)
721     p.stdin.close()
722     p.wait()
723
724 random.seed() #sigh