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

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

cut a leftover comment, fix a bit of spacing

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