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