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