main.py: small Python style improvement
[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.afs.cells[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         def __getattr__(self, name):
252             """Synthesize attributes to allow RESTful URLs like
253             /machine/13/info. This is hairy. CherryPy 3.2 adds a
254             method called _cp_dispatch that allows you to explicitly
255             handle URLs that can't be mapped, and it allows you to
256             rewrite the path components and continue processing.
257
258             This function gets the next path component being resolved
259             as a string. _cp_dispatch will get an array of strings
260             representing any subsequent path components as well."""
261
262             try:
263                 cherrypy.request.params['machine_id'] = int(name)
264                 return self
265             except ValueError:
266                 return None
267
268         @cherrypy.expose
269         @cherrypy.tools.mako(filename="/info.mako")
270         def info(self, machine_id):
271             """Handler for info on a single VM."""
272             machine = validation.Validate(cherrypy.request.login,
273                                           cherrypy.request.state,
274                                           machine_id=machine_id).machine
275             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
276             checkpoint.checkpoint('Got infodict')
277             return d
278         index = info
279
280         @cherrypy.expose
281         @cherrypy.tools.mako(filename="/info.mako")
282         @cherrypy.tools.require_POST()
283         def modify(self, machine_id, **fields):
284             """Handler for modifying attributes of a machine."""
285             try:
286                 modify_dict = modifyDict(cherrypy.request.login,
287                                          cherrypy.request.state,
288                                          machine_id, fields)
289             except InvalidInput, err:
290                 result = None
291                 machine = validation.Validate(cherrypy.request.login,
292                                               cherrypy.request.state,
293                                               machine_id=machine_id).machine
294             else:
295                 machine = modify_dict['machine']
296                 result = 'Success!'
297                 err = None
298             info_dict = infoDict(cherrypy.request.login,
299                                  cherrypy.request.state, machine)
300             info_dict['err'] = err
301             if err:
302                 for field, value in fields.items():
303                     setattr(info_dict['defaults'], field, value)
304             info_dict['result'] = result
305             return info_dict
306
307         @cherrypy.expose
308         @cherrypy.tools.mako(filename="/vnc.mako")
309         def vnc(self, machine_id):
310             """VNC applet page.
311
312             Note that due to same-domain restrictions, the applet connects to
313             the webserver, which needs to forward those requests to the xen
314             server.  The Xen server runs another proxy that (1) authenticates
315             and (2) finds the correct port for the VM.
316
317             You might want iptables like:
318
319             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
320             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
321             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
322             --dport 10003 -j SNAT --to-source 18.187.7.142
323             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
324             --dport 10003 -j ACCEPT
325
326             Remember to enable iptables!
327             echo 1 > /proc/sys/net/ipv4/ip_forward
328             """
329             machine = validation.Validate(cherrypy.request.login,
330                                           cherrypy.request.state,
331                                           machine_id=machine_id).machine
332             token = controls.vnctoken(machine)
333             host = controls.listHost(machine)
334             if host:
335                 port = 10003 + [h.hostname for h in config.hosts].index(host)
336             else:
337                 port = 5900 # dummy
338
339             status = controls.statusInfo(machine)
340             has_vnc = hasVnc(status)
341
342             d = dict(on=status,
343                      has_vnc=has_vnc,
344                      machine=machine,
345                      hostname=cherrypy.request.local.name,
346                      port=port,
347                      authtoken=token)
348             return d
349
350         @cherrypy.expose
351         @cherrypy.tools.mako(filename="/command.mako")
352         @cherrypy.tools.require_POST()
353         def command(self, command_name, machine_id, **kwargs):
354             """Handler for running commands like boot and delete on a VM."""
355             back = kwargs.get('back')
356             try:
357                 d = controls.commandResult(cherrypy.request.login,
358                                            cherrypy.request.state,
359                                            command_name, machine_id, kwargs)
360                 if d['command'] == 'Delete VM':
361                     back = 'list'
362             except InvalidInput, err:
363                 if not back:
364                     raise
365                 print >> sys.stderr, err
366                 result = str(err)
367             else:
368                 result = 'Success!'
369                 if not back:
370                     return d
371             if back == 'list':
372                 cherrypy.request.state.clear() #Changed global state
373                 raise cherrypy.InternalRedirect('/list?result=%s'
374                                                 % urllib.quote(result))
375             elif back == 'info':
376                 raise cherrypy.HTTPRedirect(cherrypy.request.base
377                                             + '/machine/%d/' % machine_id,
378                                             status=303)
379             else:
380                 raise InvalidInput('back', back, 'Not a known back page.')
381
382     machine = MachineView()
383
384 class Checkpoint:
385     def __init__(self):
386         self.start_time = time.time()
387         self.checkpoints = []
388
389     def checkpoint(self, s):
390         self.checkpoints.append((s, time.time()))
391
392     def __str__(self):
393         return ('Timing info:\n%s\n' %
394                 '\n'.join(['%s: %s' % (d, t - self.start_time) for
395                            (d, t) in self.checkpoints]))
396
397 checkpoint = Checkpoint()
398
399 class Defaults:
400     """Class to store default values for fields."""
401     memory = 256
402     disk = 4.0
403     cdrom = ''
404     autoinstall = ''
405     name = ''
406     description = ''
407     administrator = ''
408     type = 'linux-hvm'
409
410     def __init__(self, max_memory=None, max_disk=None, **kws):
411         if max_memory is not None:
412             self.memory = min(self.memory, max_memory)
413         if max_disk is not None:
414             self.disk = min(self.disk, max_disk)
415         for key in kws:
416             setattr(self, key, kws[key])
417
418 def hasVnc(status):
419     """Does the machine with a given status list support VNC?"""
420     if status is None:
421         return False
422     for l in status:
423         if l[0] == 'device' and l[1][0] == 'vfb':
424             d = dict(l[1][1:])
425             return 'location' in d
426     return False
427
428
429 def getListDict(username, state):
430     """Gets the list of local variables used by list.tmpl."""
431     checkpoint.checkpoint('Starting')
432     machines = state.machines
433     checkpoint.checkpoint('Got my machines')
434     on = {}
435     has_vnc = {}
436     installing = {}
437     xmlist = state.xmlist
438     checkpoint.checkpoint('Got uptimes')
439     for m in machines:
440         if m not in xmlist:
441             has_vnc[m] = 'Off'
442             m.uptime = None
443         else:
444             m.uptime = xmlist[m]['uptime']
445             installing[m] = bool(xmlist[m].get('autoinstall'))
446             if xmlist[m]['console']:
447                 has_vnc[m] = True
448             elif m.type.hvm:
449                 has_vnc[m] = "WTF?"
450             else:
451                 has_vnc[m] = "ParaVM"
452     max_memory = validation.maxMemory(username, state)
453     max_disk = validation.maxDisk(username)
454     checkpoint.checkpoint('Got max mem/disk')
455     defaults = Defaults(max_memory=max_memory,
456                         max_disk=max_disk,
457                         owner=username)
458     checkpoint.checkpoint('Got defaults')
459     def sortkey(machine):
460         return (machine.owner != username, machine.owner, machine.name)
461     machines = sorted(machines, key=sortkey)
462     d = dict(user=username,
463              cant_add_vm=validation.cantAddVm(username, state),
464              max_memory=max_memory,
465              max_disk=max_disk,
466              defaults=defaults,
467              machines=machines,
468              has_vnc=has_vnc,
469              installing=installing)
470     return d
471
472 def getHostname(nic):
473     """Find the hostname associated with a NIC.
474
475     XXX this should be merged with the similar logic in DNS and DHCP.
476     """
477     if nic.hostname:
478         hostname = nic.hostname
479     elif nic.machine:
480         hostname = nic.machine.name
481     else:
482         return None
483     if '.' in hostname:
484         return hostname
485     else:
486         return hostname + '.' + config.dns.domains[0]
487
488 def getNicInfo(data_dict, machine):
489     """Helper function for info, get data on nics for a machine.
490
491     Modifies data_dict to include the relevant data, and returns a list
492     of (key, name) pairs to display "name: data_dict[key]" to the user.
493     """
494     data_dict['num_nics'] = len(machine.nics)
495     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
496                            ('nic%s_mac', 'NIC %s MAC Addr'),
497                            ('nic%s_ip', 'NIC %s IP'),
498                            ]
499     nic_fields = []
500     for i in range(len(machine.nics)):
501         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
502         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
503         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
504         data_dict['nic%s_ip' % i] = machine.nics[i].ip
505     if len(machine.nics) == 1:
506         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
507     return nic_fields
508
509 def getDiskInfo(data_dict, machine):
510     """Helper function for info, get data on disks for a machine.
511
512     Modifies data_dict to include the relevant data, and returns a list
513     of (key, name) pairs to display "name: data_dict[key]" to the user.
514     """
515     data_dict['num_disks'] = len(machine.disks)
516     disk_fields_template = [('%s_size', '%s size')]
517     disk_fields = []
518     for disk in machine.disks:
519         name = disk.guest_device_name
520         disk_fields.extend([(x % name, y % name) for x, y in
521                             disk_fields_template])
522         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
523     return disk_fields
524
525 def modifyDict(username, state, machine_id, fields):
526     """Modify a machine as specified by CGI arguments.
527
528     Return a dict containing the machine that was modified.
529     """
530     olddisk = {}
531     session.begin()
532     try:
533         kws = dict([(kw, fields[kw]) for kw in
534          'owner admin contact name description memory vmtype disksize'.split()
535                     if fields[kw]])
536         kws['machine_id'] = machine_id
537         validate = validation.Validate(username, state, **kws)
538         machine = validate.machine
539         oldname = machine.name
540
541         if hasattr(validate, 'memory'):
542             machine.memory = validate.memory
543
544         if hasattr(validate, 'vmtype'):
545             machine.type = validate.vmtype
546
547         if hasattr(validate, 'disksize'):
548             disksize = validate.disksize
549             disk = machine.disks[0]
550             if disk.size != disksize:
551                 olddisk[disk.guest_device_name] = disksize
552                 disk.size = disksize
553                 session.save_or_update(disk)
554
555         update_acl = False
556         if hasattr(validate, 'owner') and validate.owner != machine.owner:
557             machine.owner = validate.owner
558             update_acl = True
559         if hasattr(validate, 'name'):
560             machine.name = validate.name
561             for n in machine.nics:
562                 if n.hostname == oldname:
563                     n.hostname = validate.name
564         if hasattr(validate, 'description'):
565             machine.description = validate.description
566         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
567             machine.administrator = validate.admin
568             update_acl = True
569         if hasattr(validate, 'contact'):
570             machine.contact = validate.contact
571
572         session.save_or_update(machine)
573         if update_acl:
574             cache_acls.refreshMachine(machine)
575         session.commit()
576     except:
577         session.rollback()
578         raise
579     for diskname in olddisk:
580         controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
581     if hasattr(validate, 'name'):
582         controls.renameMachine(machine, oldname, validate.name)
583     return dict(machine=machine)
584
585 def infoDict(username, state, machine):
586     """Get the variables used by info.tmpl."""
587     status = controls.statusInfo(machine)
588     checkpoint.checkpoint('Getting status info')
589     has_vnc = hasVnc(status)
590     if status is None:
591         main_status = dict(name=machine.name,
592                            memory=str(machine.memory))
593         uptime = None
594         cputime = None
595     else:
596         main_status = dict(status[1:])
597         main_status['host'] = controls.listHost(machine)
598         start_time = float(main_status.get('start_time', 0))
599         uptime = datetime.timedelta(seconds=int(time.time()-start_time))
600         cpu_time_float = float(main_status.get('cpu_time', 0))
601         cputime = datetime.timedelta(seconds=int(cpu_time_float))
602     checkpoint.checkpoint('Status')
603     display_fields = [('name', 'Name'),
604                       ('description', 'Description'),
605                       ('owner', 'Owner'),
606                       ('administrator', 'Administrator'),
607                       ('contact', 'Contact'),
608                       ('type', 'Type'),
609                       'NIC_INFO',
610                       ('uptime', 'uptime'),
611                       ('cputime', 'CPU usage'),
612                       ('host', 'Hosted on'),
613                       ('memory', 'RAM'),
614                       'DISK_INFO',
615                       ('state', 'state (xen format)'),
616                       ]
617     fields = []
618     machine_info = {}
619     machine_info['name'] = machine.name
620     machine_info['description'] = machine.description
621     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
622     machine_info['owner'] = machine.owner
623     machine_info['administrator'] = machine.administrator
624     machine_info['contact'] = machine.contact
625
626     nic_fields = getNicInfo(machine_info, machine)
627     nic_point = display_fields.index('NIC_INFO')
628     display_fields = (display_fields[:nic_point] + nic_fields +
629                       display_fields[nic_point+1:])
630
631     disk_fields = getDiskInfo(machine_info, machine)
632     disk_point = display_fields.index('DISK_INFO')
633     display_fields = (display_fields[:disk_point] + disk_fields +
634                       display_fields[disk_point+1:])
635
636     main_status['memory'] += ' MiB'
637     for field, disp in display_fields:
638         if field in ('uptime', 'cputime') and locals()[field] is not None:
639             fields.append((disp, locals()[field]))
640         elif field in machine_info:
641             fields.append((disp, machine_info[field]))
642         elif field in main_status:
643             fields.append((disp, main_status[field]))
644         else:
645             pass
646             #fields.append((disp, None))
647
648     checkpoint.checkpoint('Got fields')
649
650
651     max_mem = validation.maxMemory(machine.owner, state, machine, False)
652     checkpoint.checkpoint('Got mem')
653     max_disk = validation.maxDisk(machine.owner, machine)
654     defaults = Defaults()
655     for name in 'machine_id name description administrator owner memory contact'.split():
656         if getattr(machine, name):
657             setattr(defaults, name, getattr(machine, name))
658     defaults.type = machine.type.type_id
659     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
660     checkpoint.checkpoint('Got defaults')
661     d = dict(user=username,
662              on=status is not None,
663              machine=machine,
664              defaults=defaults,
665              has_vnc=has_vnc,
666              uptime=str(uptime),
667              ram=machine.memory,
668              max_mem=max_mem,
669              max_disk=max_disk,
670              fields = fields)
671     return d
672
673 def send_error_mail(subject, body):
674     import subprocess
675
676     to = config.web.errormail
677     mail = """To: %s
678 From: root@%s
679 Subject: %s
680
681 %s
682 """ % (to, config.web.hostname, subject, body)
683     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
684                          stdin=subprocess.PIPE)
685     p.stdin.write(mail)
686     p.stdin.close()
687     p.wait()
688
689 random.seed() #sigh