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