Drop privileges in VNC proxy if requested
[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         def vnc(self, machine_id):
323             """VNC applet page"""
324             return self._vnc(machine_id)
325
326         @cherrypy.expose
327         @cherrypy.tools.response_headers(headers=[('Content-Disposition', 'attachment; filename=vnc.jnlp')])
328         @cherrypy.tools.mako(filename="/vnc_jnlp.mako", content_type="application/x-java-jnlp-file")
329         def vnc_jnlp(self, machine_id):
330             """VNC applet exposed as a Java Web Start app (JNLP file)"""
331             return self._vnc(machine_id)
332
333         def _vnc(self, machine_id):
334             """VNC applet page functionality.
335
336             Note that due to same-domain restrictions, the applet connects to
337             the webserver, which needs to forward those requests to the xen
338             server.  The Xen server runs another proxy that (1) authenticates
339             and (2) finds the correct port for the VM.
340
341             You might want iptables like:
342
343             -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
344             --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
345             -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
346             --dport 10003 -j SNAT --to-source 18.187.7.142
347             -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
348             --dport 10003 -j ACCEPT
349
350             Remember to enable iptables!
351             echo 1 > /proc/sys/net/ipv4/ip_forward
352             """
353             machine = validation.Validate(cherrypy.request.login,
354                                           cherrypy.request.state,
355                                           machine_id=machine_id).machine
356             token = controls.vnctoken(machine)
357             host = controls.listHost(machine)
358             if host:
359                 port = config.vnc.base_port + [h.hostname for h in config.hosts].index(host)
360             else:
361                 port = 5900 # dummy
362
363             status = controls.statusInfo(machine)
364             has_vnc = hasVnc(status)
365
366             d = dict(on=status,
367                      has_vnc=has_vnc,
368                      machine=machine,
369                      hostname=cherrypy.request.local.name,
370                      port=port,
371                      authtoken=token)
372             return d
373
374         @cherrypy.expose
375         @cherrypy.tools.mako(filename="/novnc.mako")
376         def novnc(self, machine_id):
377             """NoVNC page functionality.
378
379             You need to set config.vnc.novnc_port and run the NoVNC proxy on
380             the web server.
381             """
382             machine = validation.Validate(cherrypy.request.login,
383                                           cherrypy.request.state,
384                                           machine_id=machine_id).machine
385             token = controls.vnctoken(machine)
386             host = controls.listHost(machine)
387
388             status = controls.statusInfo(machine)
389             has_vnc = hasVnc(status)
390
391             d = dict(on=status,
392                      has_vnc=has_vnc,
393                      machine=machine,
394                      hostname=cherrypy.request.local.name,
395                      vnc_host=host,
396                      authtoken=token)
397             return d
398
399         @cherrypy.expose
400         @cherrypy.tools.mako(filename="/command.mako")
401         @cherrypy.tools.require_POST()
402         def command(self, command_name, machine_id, **kwargs):
403             """Handler for running commands like boot and delete on a VM."""
404             back = kwargs.get('back')
405             if command_name == 'delete':
406                 back = 'list'
407             try:
408                 d = controls.commandResult(cherrypy.request.login,
409                                            cherrypy.request.state,
410                                            command_name, machine_id, kwargs)
411             except InvalidInput, err:
412                 if not back:
413                     raise
414                 print >> sys.stderr, err
415                 result = str(err)
416             else:
417                 result = 'Success!'
418                 if 'result' in d:
419                     result = d['result']
420                 if not back:
421                     return d
422             if back == 'list':
423                 cherrypy.request.state.clear() #Changed global state
424                 raise cherrypy.InternalRedirect('/list?result=%s'
425                                                 % urllib.quote(result))
426             elif back == 'info':
427                 url = cherrypy.request.base + '/machine/%d/' % machine_id
428                 if result:
429                     url += '?result='+urllib.quote(result)
430                 raise cherrypy.HTTPRedirect(url,
431                                             status=303)
432             else:
433                 raise InvalidInput('back', back, 'Not a known back page.')
434
435     machine = MachineView()
436
437
438 class Defaults:
439     """Class to store default values for fields."""
440     memory = 256
441     disk = 4.0
442     cdrom = ''
443     autoinstall = ''
444     name = ''
445     description = ''
446     administrator = ''
447     type = 'linux-hvm'
448
449     def __init__(self, max_memory=None, max_disk=None, **kws):
450         if max_memory is not None:
451             self.memory = min(self.memory, max_memory)
452         if max_disk is not None:
453             self.disk = min(self.disk, max_disk)
454         for key in kws:
455             setattr(self, key, kws[key])
456
457 def hasVnc(status):
458     """Does the machine with a given status list support VNC?"""
459     if status is None:
460         return False
461     for l in status:
462         if l[0] == 'device' and l[1][0] == 'vfb':
463             d = dict(l[1][1:])
464             return 'location' in d
465     return False
466
467
468 def getListDict(username, state):
469     """Gets the list of local variables used by list.tmpl."""
470     machines = state.machines
471     on = {}
472     has_vnc = {}
473     installing = {}
474     xmlist = state.xmlist
475     for m in machines:
476         if m not in xmlist:
477             has_vnc[m] = 'Off'
478             m.uptime = None
479         else:
480             m.uptime = xmlist[m]['uptime']
481             installing[m] = bool(xmlist[m].get('autoinstall'))
482             if xmlist[m]['console']:
483                 has_vnc[m] = True
484             elif m.type.hvm:
485                 has_vnc[m] = "WTF?"
486             else:
487                 has_vnc[m] = "ParaVM"
488     max_memory = validation.maxMemory(username, state)
489     max_disk = validation.maxDisk(username)
490     defaults = Defaults(max_memory=max_memory,
491                         max_disk=max_disk,
492                         owner=username)
493     def sortkey(machine):
494         return (machine.owner != username, machine.owner, machine.name)
495     machines = sorted(machines, key=sortkey)
496     d = dict(user=username,
497              cant_add_vm=validation.cantAddVm(username, state),
498              max_memory=max_memory,
499              max_disk=max_disk,
500              defaults=defaults,
501              machines=machines,
502              has_vnc=has_vnc,
503              installing=installing,
504              disable_creation=False)
505     return d
506
507 def getHostname(nic):
508     """Find the hostname associated with a NIC.
509
510     XXX this should be merged with the similar logic in DNS and DHCP.
511     """
512     if nic.hostname:
513         hostname = nic.hostname
514     elif nic.machine:
515         hostname = nic.machine.name
516     else:
517         return None
518     if '.' in hostname:
519         return hostname
520     else:
521         return hostname + '.' + config.dns.domains[0]
522
523 def getNicInfo(data_dict, machine):
524     """Helper function for info, get data on nics for a machine.
525
526     Modifies data_dict to include the relevant data, and returns a list
527     of (key, name) pairs to display "name: data_dict[key]" to the user.
528     """
529     data_dict['num_nics'] = len(machine.nics)
530     nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
531                            ('nic%s_mac', 'NIC %s MAC Addr'),
532                            ('nic%s_ip', 'NIC %s IP'),
533                            ('nic%s_netmask', 'NIC %s Netmask'),
534                            ('nic%s_gateway', 'NIC %s Gateway'),
535                            ]
536     nic_fields = []
537     for i in range(len(machine.nics)):
538         nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
539         data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
540         data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
541         data_dict['nic%s_ip' % i] = machine.nics[i].ip
542         data_dict['nic%s_netmask' % i] = machine.nics[i].netmask
543         data_dict['nic%s_gateway' % i] = machine.nics[i].gateway
544         if machine.nics[i].other_ip:
545             nic_fields.append(('nic%s_other' % i, 'NIC %s Other Address' % i))
546             other = '%s/%s via %s' % (machine.nics[i].other_ip, machine.nics[i].other_netmask, machine.nics[i].other_gateway)
547             other_action = machine.nics[i].other_action
548             if other_action == 'dnat':
549                 other += " (NAT to primary IP)"
550             elif other_action == 'renumber':
551                 other += " (cold boot or renew DHCP lease to swap)"
552             elif other_action == 'renumber_dhcp':
553                 other += " (renew DHCP lease to swap)"
554             elif other_action == 'remove':
555                 other += " (will be removed at next cold boot or DHCP lease renewal)"
556             else:
557                 other += " (pending assignment)"
558             data_dict['nic%s_other' % i] = other
559     if len(machine.nics) == 1:
560         nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
561     return nic_fields
562
563 def getDiskInfo(data_dict, machine):
564     """Helper function for info, get data on disks for a machine.
565
566     Modifies data_dict to include the relevant data, and returns a list
567     of (key, name) pairs to display "name: data_dict[key]" to the user.
568     """
569     data_dict['num_disks'] = len(machine.disks)
570     disk_fields_template = [('%s_size', '%s size')]
571     disk_fields = []
572     for disk in machine.disks:
573         name = disk.guest_device_name
574         disk_fields.extend([(x % name, y % name) for x, y in
575                             disk_fields_template])
576         data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
577     return disk_fields
578
579 def modifyDict(username, state, machine_id, fields):
580     """Modify a machine as specified by CGI arguments.
581
582     Return a dict containing the machine that was modified.
583     """
584     olddisk = {}
585     session.begin()
586     try:
587         kws = dict((kw, fields[kw]) for kw in
588          'owner admin contact name description memory vmtype disksize'.split()
589                     if fields.get(kw))
590         kws['machine_id'] = machine_id
591         validate = validation.Validate(username, state, **kws)
592         machine = validate.machine
593         oldname = machine.name
594
595         if hasattr(validate, 'memory'):
596             machine.memory = validate.memory
597
598         if hasattr(validate, 'vmtype'):
599             machine.type = validate.vmtype
600
601         update_acl = False
602         if hasattr(validate, 'owner') and validate.owner != machine.owner:
603             machine.owner = validate.owner
604             update_acl = True
605         if hasattr(validate, 'description'):
606             machine.description = validate.description
607         if hasattr(validate, 'admin') and validate.admin != machine.administrator:
608             machine.administrator = validate.admin
609             update_acl = True
610         if hasattr(validate, 'contact'):
611             machine.contact = validate.contact
612
613         session.add(machine)
614         session.commit()
615     except:
616         session.rollback()
617         raise
618
619     session.begin()
620     try:
621         if hasattr(validate, 'disksize'):
622             disksize = validate.disksize
623             disk = machine.disks[0]
624             if disk.size != disksize:
625                 olddisk[disk.guest_device_name] = disksize
626                 disk.size = disksize
627                 session.add(disk)
628         for diskname in olddisk:
629             controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
630         session.add(machine)
631         session.commit()
632     except:
633         session.rollback()
634         raise
635
636     session.begin()
637     try:
638         if hasattr(validate, 'name'):
639             machine.name = validate.name
640             for n in machine.nics:
641                 if n.hostname == oldname:
642                     n.hostname = validate.name
643         if hasattr(validate, 'name'):
644             controls.renameMachine(machine, oldname, validate.name)
645         session.add(machine)
646         session.commit()
647     except:
648         session.rollback()
649         raise
650
651     if update_acl:
652         cache_acls.refreshMachine(machine)
653
654     return dict(machine=machine)
655
656 def infoDict(username, state, machine):
657     """Get the variables used by info.tmpl."""
658     try:
659         status = controls.statusInfo(machine)
660     except CodeError, e:
661         # machine was shut down in between the call to listInfoDict and this
662         status = None
663     has_vnc = hasVnc(status)
664     if status is None:
665         main_status = dict(name=machine.name,
666                            memory=str(machine.memory))
667         uptime = None
668         cputime = None
669     else:
670         main_status = dict(status[1:])
671         main_status['host'] = controls.listHost(machine)
672         start_time = main_status.get('start_time')
673         if start_time is None:
674             uptime = "Still booting?"
675         else:
676             start_time = float(start_time)
677             uptime = datetime.timedelta(seconds=int(time.time()-start_time))
678         cpu_time_float = float(main_status.get('cpu_time', 0))
679         cputime = datetime.timedelta(seconds=int(cpu_time_float))
680     display_fields = [('name', 'Name'),
681                       ('description', 'Description'),
682                       ('owner', 'Owner'),
683                       ('administrator', 'Administrator'),
684                       ('contact', 'Contact'),
685                       ('type', 'Type'),
686                       'NIC_INFO',
687                       ('uptime', 'uptime'),
688                       ('cputime', 'CPU usage'),
689                       ('host', 'Hosted on'),
690                       ('memory', 'RAM'),
691                       'DISK_INFO',
692                       ('state', 'state (xen format)'),
693                       ]
694     fields = []
695     machine_info = {}
696     machine_info['name'] = machine.name
697     machine_info['description'] = machine.description
698     machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
699     machine_info['owner'] = machine.owner
700     machine_info['administrator'] = machine.administrator
701     machine_info['contact'] = machine.contact
702
703     nic_fields = getNicInfo(machine_info, machine)
704     nic_point = display_fields.index('NIC_INFO')
705     display_fields = (display_fields[:nic_point] + nic_fields +
706                       display_fields[nic_point+1:])
707
708     disk_fields = getDiskInfo(machine_info, machine)
709     disk_point = display_fields.index('DISK_INFO')
710     display_fields = (display_fields[:disk_point] + disk_fields +
711                       display_fields[disk_point+1:])
712
713     renumber = False
714     for n in machine.nics:
715         if n.other_action == 'renumber_dhcp':
716             renumber = True
717
718     main_status['memory'] += ' MiB'
719     for field, disp in display_fields:
720         if field in ('uptime', 'cputime') and locals()[field] is not None:
721             fields.append((disp, locals()[field]))
722         elif field in machine_info:
723             fields.append((disp, machine_info[field]))
724         elif field in main_status:
725             fields.append((disp, main_status[field]))
726         else:
727             pass
728             #fields.append((disp, None))
729
730     max_mem = validation.maxMemory(machine.owner, state, machine, False)
731     max_disk = validation.maxDisk(machine.owner, machine)
732     defaults = Defaults()
733     for name in 'machine_id name description administrator owner memory contact'.split():
734         if getattr(machine, name):
735             setattr(defaults, name, getattr(machine, name))
736     defaults.type = machine.type.type_id
737     defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
738     d = dict(user=username,
739              on=status is not None,
740              renumber=renumber,
741              machine=machine,
742              defaults=defaults,
743              has_vnc=has_vnc,
744              uptime=str(uptime),
745              ram=machine.memory,
746              max_mem=max_mem,
747              max_disk=max_disk,
748              fields = fields)
749     return d
750
751 def send_error_mail(subject, body):
752     import subprocess
753
754     to = config.web.errormail
755     mail = """To: %s
756 From: root@%s
757 Subject: %s
758
759 %s
760 """ % (to, config.web.hostname, subject, body)
761     p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
762                          stdin=subprocess.PIPE)
763     p.stdin.write(mail)
764     p.stdin.close()
765     p.wait()
766
767 random.seed() #sigh