source: package_branches/invirt-web/ajaxterm-rebased/code/main.py @ 2753

Last change on this file since 2753 was 2753, checked in by broder, 14 years ago

Allow reconnecting to the same terminal session

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