Add Java Web Start for Java HVM console
[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
41
42 static_dir = os.path.join(os.path.dirname(__file__), 'static')
43 InvirtStatic = cherrypy.tools.staticdir.handler(
44     root=static_dir,
45     dir=static_dir,
46     section='/static')
47
48 class InvirtUnauthWeb(View):
49     static = InvirtStatic
50
51     @cherrypy.expose
52     @cherrypy.tools.mako(filename="/unauth.mako")
53     def index(self):
54         return dict(simple=True)
55
56 class InvirtWeb(View):
57     def __init__(self):
58         super(self.__class__,self).__init__()
59         connect()
60         self._cp_config['tools.require_login.on'] = True
61         self._cp_config['tools.catch_stderr.on'] = True
62         self._cp_config['tools.mako.imports'] = ['from invirt.config import structs as config',
63                                                  'from invirt import database']
64         self._cp_config['request.error_response'] = self.handle_error
65
66     static = InvirtStatic
67
68     @cherrypy.expose
69     @cherrypy.tools.mako(filename="/invalid.mako")
70     def invalidInput(self):
71         """Print an error page when an InvalidInput exception occurs"""
72         err = cherrypy.request.prev.params["err"]
73         emsg = cherrypy.request.prev.params["emsg"]
74         d = dict(err_field=err.err_field,
75                  err_value=str(err.err_value), stderr=emsg,
76                  errorMessage=str(err))
77         return d
78
79     @cherrypy.expose
80     @cherrypy.tools.mako(filename="/error.mako")
81     def error(self):
82         """Print an error page when an exception occurs"""
83         op = cherrypy.request.prev.path_info
84         username = cherrypy.request.login
85         err = cherrypy.request.prev.params["err"]
86         emsg = cherrypy.request.prev.params["emsg"]
87         traceback = cherrypy.request.prev.params["traceback"]
88         d = dict(op=op, user=username, fields=cherrypy.request.prev.params,
89                  errorMessage=str(err), stderr=emsg, traceback=traceback)
90         error_raw = cherrypy.request.lookup.get_template("/error_raw.mako")
91         details = error_raw.render(**d)
92         exclude = config.web.errormail_exclude
93         if username not in exclude and '*' not in exclude:
94             send_error_mail('xvm error on %s for %s: %s' % (op, cherrypy.request.login, err),
95                             details)
96         d['details'] = details
97         return d
98
99     def __getattr__(self, name):
100         # At the point __getattr__ is called, tools haven't been run. Make sure the user is logged in.
101         cherrypy.tools.remote_user_login.callable()
102
103         if name in ("admin", "overlord"):
104             if not cherrypy.request.login in getAfsGroupMembers(config.adminacl, config.authz.afs.cells[0].cell):
105                 raise InvalidInput('username', cherrypy.request.login,
106                                    'Not in admin group %s.' % config.adminacl)
107             cherrypy.request.state = State(cherrypy.request.login, isadmin=True)
108             return self
109         else:
110             return super(InvirtWeb, self).__getattr__(name)
111
112     def handle_error(self):
113         err = sys.exc_info()[1]
114         if isinstance(err, InvalidInput):
115             cherrypy.request.params['err'] = err
116             cherrypy.request.params['emsg'] = revertStandardError()
117             raise cherrypy.InternalRedirect('/invalidInput')
118         if not cherrypy.request.prev or 'err' not in cherrypy.request.prev.params:
119             cherrypy.request.params['err'] = err
120             cherrypy.request.params['emsg'] = revertStandardError()
121             cherrypy.request.params['traceback'] = _cperror.format_exc()
122             raise cherrypy.InternalRedirect('/error')
123         # fall back to cherrypy default error page
124         cherrypy.HTTPError(500).set_response()
125
126     @cherrypy.expose
127     @cherrypy.tools.mako(filename="/list.mako")
128     def list(self, result=None):
129         """Handler for list requests."""
130         d = getListDict(cherrypy.request.login, cherrypy.request.state)
131         if result is not None:
132             d['result'] = result
133         return d
134     index=list
135
136     @cherrypy.expose
137     @cherrypy.tools.mako(filename="/help.mako")
138     def help(self, subject=None, simple=False):
139         """Handler for help messages."""
140
141         help_mapping = {
142             'Autoinstalls': """
143 The autoinstaller builds a minimal Debian or Ubuntu system to run as a
144 ParaVM.  You can access the resulting system by logging into the <a
145 href="help?simple=true&subject=ParaVM+Console">serial console server</a>
146 with your Kerberos tickets; there is no root password so sshd will
147 refuse login.</p>
148
149 <p>Under the covers, the autoinstaller uses our own patched version of
150 xen-create-image, which is a tool based on debootstrap.  If you log
151 into the serial console while the install is running, you can watch
152 it.
153 """,
154             'ParaVM Console': """
155 ParaVM machines do not support local console access over VNC.  To
156 access the serial console of these machines, you can SSH with Kerberos
157 to %s, using the name of the machine as your
158 username.""" % config.console.hostname,
159             'HVM/ParaVM': """
160 HVM machines use the virtualization features of the processor, while
161 ParaVM machines rely on a modified kernel to communicate directly with
162 the hypervisor.  HVMs support boot CDs of any operating system, and
163 the VNC console applet.  The three-minute autoinstaller produces
164 ParaVMs.  ParaVMs typically are more efficient, and always support the
165 <a href="help?subject=ParaVM+Console">console server</a>.</p>
166
167 <p>More details are <a
168 href="https://xvm.scripts.mit.edu/wiki/Paravirtualization">on the
169 wiki</a>, including steps to prepare an HVM guest to boot as a ParaVM
170 (which you can skip by using the autoinstaller to begin with.)</p>
171
172 <p>We recommend using a ParaVM when possible and an HVM when necessary.
173 """,
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 7:</strong> The Windows 7 image is licensed for all MIT students and will automatically activate off the network; see <a href="/static/msca-7.txt">the licensing agreement</a> for details. The installer requires 512 MiB RAM and at least 15 GiB disk space (20 GiB or more recommended).<br>
198 <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>
199 <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.
200 """
201             }
202
203         if not subject:
204             subject = sorted(help_mapping.keys())
205         if not isinstance(subject, list):
206             subject = [subject]
207
208         return dict(simple=simple,
209                     subjects=subject,
210                     mapping=help_mapping)
211     help._cp_config['tools.require_login.on'] = False
212
213     def parseCreate(self, fields):
214         kws = dict([(kw, fields[kw]) for kw in
215          'name description owner memory disksize vmtype cdrom autoinstall'.split()
216                     if fields[kw]])
217         validate = validation.Validate(cherrypy.request.login,
218                                        cherrypy.request.state,
219                                        strict=True, **kws)
220         return dict(contact=cherrypy.request.login, name=validate.name,
221                     description=validate.description, memory=validate.memory,
222                     disksize=validate.disksize, owner=validate.owner,
223                     machine_type=getattr(validate, 'vmtype', Defaults.type),
224                     cdrom=getattr(validate, 'cdrom', None),
225                     autoinstall=getattr(validate, 'autoinstall', None))
226
227     @cherrypy.expose
228     @cherrypy.tools.mako(filename="/list.mako")
229     @cherrypy.tools.require_POST()
230     def create(self, **fields):
231         """Handler for create requests."""
232         try:
233             parsed_fields = self.parseCreate(fields)
234             machine = controls.createVm(cherrypy.request.login,
235                                         cherrypy.request.state, **parsed_fields)
236         except InvalidInput, err:
237             pass
238         else:
239             err = None
240         cherrypy.request.state.clear() #Changed global state
241         d = getListDict(cherrypy.request.login, cherrypy.request.state)
242         d['err'] = err
243         if err:
244             for field, value in fields.items():
245                 setattr(d['defaults'], field, value)
246         else:
247             d['new_machine'] = parsed_fields['name']
248         return d
249
250     @cherrypy.expose
251     @cherrypy.tools.mako(filename="/helloworld.mako")
252     def helloworld(self, **kwargs):
253         return {'request': cherrypy.request, 'kwargs': kwargs}
254     helloworld._cp_config['tools.require_login.on'] = False
255
256     @cherrypy.expose
257     def errortest(self):
258         """Throw an error, to test the error-tracing mechanisms."""
259         print >>sys.stderr, "look ma, it's a stderr"
260         raise RuntimeError("test of the emergency broadcast system")
261
262     class MachineView(View):
263         def __getattr__(self, name):
264             """Synthesize attributes to allow RESTful URLs like
265             /machine/13/info. This is hairy. CherryPy 3.2 adds a
266             method called _cp_dispatch that allows you to explicitly
267             handle URLs that can't be mapped, and it allows you to
268             rewrite the path components and continue processing.
269
270             This function gets the next path component being resolved
271             as a string. _cp_dispatch will get an array of strings
272             representing any subsequent path components as well."""
273
274             try:
275                 cherrypy.request.params['machine_id'] = int(name)
276                 return self
277             except ValueError:
278                 return None
279
280         @cherrypy.expose
281         @cherrypy.tools.mako(filename="/info.mako")
282         def info(self, machine_id, result=None):
283             """Handler for info on a single VM."""
284             machine = validation.Validate(cherrypy.request.login,
285                                           cherrypy.request.state,
286                                           machine_id=machine_id).machine
287             d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
288             if result:
289                 d['result'] = result
290             return d
291         index = info
292
293         @cherrypy.expose
294         @cherrypy.tools.mako(filename="/info.mako")
295         @cherrypy.tools.require_POST()
296         def modify(self, machine_id, **fields):
297             """Handler for modifying attributes of a machine."""
298             try:
299                 modify_dict = modifyDict(cherrypy.request.login,
300                                          cherrypy.request.state,
301                                          machine_id, fields)
302             except InvalidInput, err:
303                 result = None
304                 machine = validation.Validate(cherrypy.request.login,
305                                               cherrypy.request.state,
306                                               machine_id=machine_id).machine
307             else:
308                 machine = modify_dict['machine']
309                 result = 'Success!'
310                 err = None
311             info_dict = infoDict(cherrypy.request.login,
312                                  cherrypy.request.state, machine)
313             info_dict['err'] = err
314             if err:
315                 for field, value in fields.items():
316                     setattr(info_dict['defaults'], field, value)
317             info_dict['result'] = result
318             return info_dict
319
320         @cherrypy.expose
321         @cherrypy.tools.mako(filename="/vnc.mako")
322
323         def vnc(self, machine_id):
324             """VNC applet page"""
325             return self._vnc(machine_id)
326
327         @cherrypy.expose
328         @cherrypy.tools.response_headers(headers=[('Content-Disposition', 'attachment; filename=vnc.jnlp')])
329         @cherrypy.tools.mako(filename="/vnc_jnlp.mako", content_type="application/x-java-jnlp-file")
330         def vnc_jnlp(self, machine_id):
331             """VNC applet exposed as a Java Web Start app (JNLP file)"""
332             return self._vnc(machine_id)
333
334         def _vnc(self, machine_id):
335             """VNC applet page functionality.
336
337             Note that due to same-domain restrictions, the applet connects to
338             the webserver, which needs to forward those requests to the xen
339             server.  The Xen server runs another proxy that (1) authenticates
340             and (2) finds the correct port for the VM.
341
342             You might want iptables like:
343
344             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
345             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
346             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
347             --dport 10003 -j SNAT --to-source 18.187.7.142
348             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
349             --dport 10003 -j ACCEPT
350
351             Remember to enable iptables!
352             echo 1 > /proc/sys/net/ipv4/ip_forward
353             """
354             machine = validation.Validate(cherrypy.request.login,
355                                           cherrypy.request.state,
356                                           machine_id=machine_id).machine
357             token = controls.vnctoken(machine)
358             host = controls.listHost(machine)
359             if host:
360                 port = 10003 + [h.hostname for h in config.hosts].index(host)
361             else:
362                 port = 5900 # dummy
363
364             status = controls.statusInfo(machine)
365             has_vnc = hasVnc(status)
366
367             d = dict(on=status,
368                      has_vnc=has_vnc,
369                      machine=machine,
370                      hostname=cherrypy.request.local.name,
371                      port=port,
372                      authtoken=token)
373             return d
374
375         @cherrypy.expose
376         @cherrypy.tools.mako(filename="/command.mako")
377         @cherrypy.tools.require_POST()
378         def command(self, command_name, machine_id, **kwargs):
379             """Handler for running commands like boot and delete on a VM."""
380             back = kwargs.get('back')
381             if command_name == 'delete':
382                 back = 'list'
383             try:
384                 d = controls.commandResult(cherrypy.request.login,
385                                            cherrypy.request.state,
386                                            command_name, machine_id, kwargs)
387             except InvalidInput, err:
388                 if not back:
389                     raise
390                 print >> sys.stderr, err
391                 result = str(err)
392             else:
393                 result = 'Success!'
394                 if 'result' in d:
395                     result = d['result']
396                 if not back:
397                     return d
398             if back == 'list':
399                 cherrypy.request.state.clear() #Changed global state
400                 raise cherrypy.InternalRedirect('/list?result=%s'
401                                                 % urllib.quote(result))
402             elif back == 'info':
403                 url = cherrypy.request.base + '/machine/%d/' % machine_id
404                 if result:
405                     url += '?result='+urllib.quote(result)
406                 raise cherrypy.HTTPRedirect(url,
407                                             status=303)
408             else:
409                 raise InvalidInput('back', back, 'Not a known back page.')
410
411     machine = MachineView()
412
413
414 class Defaults:
415     """Class to store default values for fields."""
416     memory = 256
417     disk = 4.0
418     cdrom = ''
419     autoinstall = ''
420     name = ''
421     description = ''
422     administrator = ''
423     type = 'linux-hvm'
424
425     def __init__(self, max_memory=None, max_disk=None, **kws):
426         if max_memory is not None:
427             self.memory = min(self.memory, max_memory)
428         if max_disk is not None:
429             self.disk = min(self.disk, max_disk)
430         for key in kws:
431             setattr(self, key, kws[key])
432
433 def hasVnc(status):
434     """Does the machine with a given status list support VNC?"""
435     if status is None:
436         return False
437     for l in status:
438         if l[0] == 'device' and l[1][0] == 'vfb':
439             d = dict(l[1][1:])
440             return 'location' in d
441     return False
442
443
444 def getListDict(username, state):
445     """Gets the list of local variables used by list.tmpl."""
446     machines = state.machines
447     on = {}
448     has_vnc = {}
449     installing = {}
450     xmlist = state.xmlist
451     for m in machines:
452         if m not in xmlist:
453             has_vnc[m] = 'Off'
454             m.uptime = None
455         else:
456             m.uptime = xmlist[m]['uptime']
457             installing[m] = bool(xmlist[m].get('autoinstall'))
458             if xmlist[m]['console']:
459                 has_vnc[m] = True
460             elif m.type.hvm:
461                 has_vnc[m] = "WTF?"
462             else:
463                 has_vnc[m] = "ParaVM"
464     max_memory = validation.maxMemory(username, state)
465     max_disk = validation.maxDisk(username)
466     defaults = Defaults(max_memory=max_memory,
467                         max_disk=max_disk,
468                         owner=username)
469     def sortkey(machine):
470         return (machine.owner != username, machine.owner, machine.name)
471     machines = sorted(machines, key=sortkey)
472     d = dict(user=username,
473              cant_add_vm=validation.cantAddVm(username, state),
474              max_memory=max_memory,
475              max_disk=max_disk,
476              defaults=defaults,
477              machines=machines,
478              has_vnc=has_vnc,
479              installing=installing,
480              disable_creation=False)
481     return d
482
483 def getHostname(nic):
484     """Find the hostname associated with a NIC.
485
486     XXX this should be merged with the similar logic in DNS and DHCP.
487     """
488     if nic.hostname:
489         hostname = nic.hostname
490     elif nic.machine:
491         hostname = nic.machine.name
492     else:
493         return None
494     if '.' in hostname:
495         return hostname
496     else:
497         return hostname + '.' + config.dns.domains[0]
498
499 def getNicInfo(data_dict, machine):
500     """Helper function for info, get data on nics for a machine.
501
502     Modifies data_dict to include the relevant data, and returns a list
503     of (key, name) pairs to display "name: data_dict[key]" to the user.
504     """
505     data_dict['num_nics'] = len(machine.nics)
506     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
507                            ('nic%s_mac', 'NIC %s MAC Addr'),
508                            ('nic%s_ip', 'NIC %s IP'),
509                            ('nic%s_netmask', 'NIC %s Netmask'),
510                            ('nic%s_gateway', 'NIC %s Gateway'),
511                            ]
512     nic_fields = []
513     for i in range(len(machine.nics)):
514         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
515         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
516         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
517         data_dict['nic%s_ip' % i] = machine.nics[i].ip
518         data_dict['nic%s_netmask' % i] = machine.nics[i].netmask
519         data_dict['nic%s_gateway' % i] = machine.nics[i].gateway
520         if machine.nics[i].other_ip:
521             nic_fields.append(('nic%s_other' % i, 'NIC %s Other Address' % i))
522             other = '%s/%s via %s' % (machine.nics[i].other_ip, machine.nics[i].other_netmask, machine.nics[i].other_gateway)
523             other_action = machine.nics[i].other_action
524             if other_action == 'dnat':
525                 other += " (NAT to primary IP)"
526             elif other_action == 'renumber':
527                 other += " (cold boot or renew DHCP lease to swap)"
528             elif other_action == 'renumber_dhcp':
529                 other += " (renew DHCP lease to swap)"
530             elif other_action == 'remove':
531                 other += " (will be removed at next cold boot or DHCP lease renewal)"
532             else:
533                 other += " (pending assignment)"
534             data_dict['nic%s_other' % i] = other
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.get(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         update_acl = False
578         if hasattr(validate, 'owner') and validate.owner != machine.owner:
579             machine.owner = validate.owner
580             update_acl = True
581         if hasattr(validate, 'description'):
582             machine.description = validate.description
583         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
584             machine.administrator = validate.admin
585             update_acl = True
586         if hasattr(validate, 'contact'):
587             machine.contact = validate.contact
588
589         session.add(machine)
590         session.commit()
591     except:
592         session.rollback()
593         raise
594
595     session.begin()
596     try:
597         if hasattr(validate, 'disksize'):
598             disksize = validate.disksize
599             disk = machine.disks[0]
600             if disk.size != disksize:
601                 olddisk[disk.guest_device_name] = disksize
602                 disk.size = disksize
603                 session.add(disk)
604         for diskname in olddisk:
605             controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
606         session.add(machine)
607         session.commit()
608     except:
609         session.rollback()
610         raise
611
612     session.begin()
613     try:
614         if hasattr(validate, 'name'):
615             machine.name = validate.name
616             for n in machine.nics:
617                 if n.hostname == oldname:
618                     n.hostname = validate.name
619         if hasattr(validate, 'name'):
620             controls.renameMachine(machine, oldname, validate.name)
621         session.add(machine)
622         session.commit()
623     except:
624         session.rollback()
625         raise
626
627     if update_acl:
628         cache_acls.refreshMachine(machine)
629
630     return dict(machine=machine)
631
632 def infoDict(username, state, machine):
633     """Get the variables used by info.tmpl."""
634     try:
635         status = controls.statusInfo(machine)
636     except CodeError, e:
637         # machine was shut down in between the call to listInfoDict and this
638         status = None
639     has_vnc = hasVnc(status)
640     if status is None:
641         main_status = dict(name=machine.name,
642                            memory=str(machine.memory))
643         uptime = None
644         cputime = None
645     else:
646         main_status = dict(status[1:])
647         main_status['host'] = controls.listHost(machine)
648         start_time = main_status.get('start_time')
649         if start_time is None:
650             uptime = "Still booting?"
651         else:
652             start_time = float(start_time)
653             uptime = datetime.timedelta(seconds=int(time.time()-start_time))
654         cpu_time_float = float(main_status.get('cpu_time', 0))
655         cputime = datetime.timedelta(seconds=int(cpu_time_float))
656     display_fields = [('name', 'Name'),
657                       ('description', 'Description'),
658                       ('owner', 'Owner'),
659                       ('administrator', 'Administrator'),
660                       ('contact', 'Contact'),
661                       ('type', 'Type'),
662                       'NIC_INFO',
663                       ('uptime', 'uptime'),
664                       ('cputime', 'CPU usage'),
665                       ('host', 'Hosted on'),
666                       ('memory', 'RAM'),
667                       'DISK_INFO',
668                       ('state', 'state (xen format)'),
669                       ]
670     fields = []
671     machine_info = {}
672     machine_info['name'] = machine.name
673     machine_info['description'] = machine.description
674     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
675     machine_info['owner'] = machine.owner
676     machine_info['administrator'] = machine.administrator
677     machine_info['contact'] = machine.contact
678
679     nic_fields = getNicInfo(machine_info, machine)
680     nic_point = display_fields.index('NIC_INFO')
681     display_fields = (display_fields[:nic_point] + nic_fields +
682                       display_fields[nic_point+1:])
683
684     disk_fields = getDiskInfo(machine_info, machine)
685     disk_point = display_fields.index('DISK_INFO')
686     display_fields = (display_fields[:disk_point] + disk_fields +
687                       display_fields[disk_point+1:])
688
689     renumber = False
690     for n in machine.nics:
691         if n.other_action == 'renumber_dhcp':
692             renumber = True
693
694     main_status['memory'] += ' MiB'
695     for field, disp in display_fields:
696         if field in ('uptime', 'cputime') and locals()[field] is not None:
697             fields.append((disp, locals()[field]))
698         elif field in machine_info:
699             fields.append((disp, machine_info[field]))
700         elif field in main_status:
701             fields.append((disp, main_status[field]))
702         else:
703             pass
704             #fields.append((disp, None))
705
706     max_mem = validation.maxMemory(machine.owner, state, machine, False)
707     max_disk = validation.maxDisk(machine.owner, machine)
708     defaults = Defaults()
709     for name in 'machine_id name description administrator owner memory contact'.split():
710         if getattr(machine, name):
711             setattr(defaults, name, getattr(machine, name))
712     defaults.type = machine.type.type_id
713     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
714     d = dict(user=username,
715              on=status is not None,
716              renumber=renumber,
717              machine=machine,
718              defaults=defaults,
719              has_vnc=has_vnc,
720              uptime=str(uptime),
721              ram=machine.memory,
722              max_mem=max_mem,
723              max_disk=max_disk,
724              fields = fields)
725     return d
726
727 def send_error_mail(subject, body):
728     import subprocess
729
730     to = config.web.errormail
731     mail = """To: %s
732 From: root@%s
733 Subject: %s
734
735 %s
736 """ % (to, config.web.hostname, subject, body)
737     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
738                          stdin=subprocess.PIPE)
739     p.stdin.write(mail)
740     p.stdin.close()
741     p.wait()
742
743 random.seed() #sigh