source: package_branches/invirt-web/cherrypy-rebased/code/main.py @ 2710

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

Restore explanatory comment on random.seed().

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