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