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

Last change on this file since 2722 was 2720, checked in by broder, 15 years ago

main.py: small Python style improvement

  • Property svn:executable set to *
File size: 26.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.afs.cells[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        def __getattr__(self, name):
252            """Synthesize attributes to allow RESTful URLs like
253            /machine/13/info. This is hairy. CherryPy 3.2 adds a
254            method called _cp_dispatch that allows you to explicitly
255            handle URLs that can't be mapped, and it allows you to
256            rewrite the path components and continue processing.
257
258            This function gets the next path component being resolved
259            as a string. _cp_dispatch will get an array of strings
260            representing any subsequent path components as well."""
261
262            try:
263                cherrypy.request.params['machine_id'] = int(name)
264                return self
265            except ValueError:
266                return None
267
268        @cherrypy.expose
269        @cherrypy.tools.mako(filename="/info.mako")
270        def info(self, machine_id):
271            """Handler for info on a single VM."""
272            machine = validation.Validate(cherrypy.request.login,
273                                          cherrypy.request.state,
274                                          machine_id=machine_id).machine
275            d = infoDict(cherrypy.request.login, cherrypy.request.state, machine)
276            checkpoint.checkpoint('Got infodict')
277            return d
278        index = info
279
280        @cherrypy.expose
281        @cherrypy.tools.mako(filename="/info.mako")
282        @cherrypy.tools.require_POST()
283        def modify(self, machine_id, **fields):
284            """Handler for modifying attributes of a machine."""
285            try:
286                modify_dict = modifyDict(cherrypy.request.login,
287                                         cherrypy.request.state,
288                                         machine_id, fields)
289            except InvalidInput, err:
290                result = None
291                machine = validation.Validate(cherrypy.request.login,
292                                              cherrypy.request.state,
293                                              machine_id=machine_id).machine
294            else:
295                machine = modify_dict['machine']
296                result = 'Success!'
297                err = None
298            info_dict = infoDict(cherrypy.request.login,
299                                 cherrypy.request.state, machine)
300            info_dict['err'] = err
301            if err:
302                for field, value in fields.items():
303                    setattr(info_dict['defaults'], field, value)
304            info_dict['result'] = result
305            return info_dict
306
307        @cherrypy.expose
308        @cherrypy.tools.mako(filename="/vnc.mako")
309        def vnc(self, machine_id):
310            """VNC applet page.
311
312            Note that due to same-domain restrictions, the applet connects to
313            the webserver, which needs to forward those requests to the xen
314            server.  The Xen server runs another proxy that (1) authenticates
315            and (2) finds the correct port for the VM.
316
317            You might want iptables like:
318
319            -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp \
320            --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
321            -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp \
322            --dport 10003 -j SNAT --to-source 18.187.7.142
323            -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp \
324            --dport 10003 -j ACCEPT
325
326            Remember to enable iptables!
327            echo 1 > /proc/sys/net/ipv4/ip_forward
328            """
329            machine = validation.Validate(cherrypy.request.login,
330                                          cherrypy.request.state,
331                                          machine_id=machine_id).machine
332            token = controls.vnctoken(machine)
333            host = controls.listHost(machine)
334            if host:
335                port = 10003 + [h.hostname for h in config.hosts].index(host)
336            else:
337                port = 5900 # dummy
338
339            status = controls.statusInfo(machine)
340            has_vnc = hasVnc(status)
341
342            d = dict(on=status,
343                     has_vnc=has_vnc,
344                     machine=machine,
345                     hostname=cherrypy.request.local.name,
346                     port=port,
347                     authtoken=token)
348            return d
349
350        @cherrypy.expose
351        @cherrypy.tools.mako(filename="/command.mako")
352        @cherrypy.tools.require_POST()
353        def command(self, command_name, machine_id, **kwargs):
354            """Handler for running commands like boot and delete on a VM."""
355            back = kwargs.get('back')
356            try:
357                d = controls.commandResult(cherrypy.request.login,
358                                           cherrypy.request.state,
359                                           command_name, machine_id, kwargs)
360                if d['command'] == 'Delete VM':
361                    back = 'list'
362            except InvalidInput, err:
363                if not back:
364                    raise
365                print >> sys.stderr, err
366                result = str(err)
367            else:
368                result = 'Success!'
369                if not back:
370                    return d
371            if back == 'list':
372                cherrypy.request.state.clear() #Changed global state
373                raise cherrypy.InternalRedirect('/list?result=%s'
374                                                % urllib.quote(result))
375            elif back == 'info':
376                raise cherrypy.HTTPRedirect(cherrypy.request.base
377                                            + '/machine/%d/' % machine_id,
378                                            status=303)
379            else:
380                raise InvalidInput('back', back, 'Not a known back page.')
381
382    machine = MachineView()
383
384class Checkpoint:
385    def __init__(self):
386        self.start_time = time.time()
387        self.checkpoints = []
388
389    def checkpoint(self, s):
390        self.checkpoints.append((s, time.time()))
391
392    def __str__(self):
393        return ('Timing info:\n%s\n' %
394                '\n'.join(['%s: %s' % (d, t - self.start_time) for
395                           (d, t) in self.checkpoints]))
396
397checkpoint = Checkpoint()
398
399class Defaults:
400    """Class to store default values for fields."""
401    memory = 256
402    disk = 4.0
403    cdrom = ''
404    autoinstall = ''
405    name = ''
406    description = ''
407    administrator = ''
408    type = 'linux-hvm'
409
410    def __init__(self, max_memory=None, max_disk=None, **kws):
411        if max_memory is not None:
412            self.memory = min(self.memory, max_memory)
413        if max_disk is not None:
414            self.disk = min(self.disk, max_disk)
415        for key in kws:
416            setattr(self, key, kws[key])
417
418def hasVnc(status):
419    """Does the machine with a given status list support VNC?"""
420    if status is None:
421        return False
422    for l in status:
423        if l[0] == 'device' and l[1][0] == 'vfb':
424            d = dict(l[1][1:])
425            return 'location' in d
426    return False
427
428
429def getListDict(username, state):
430    """Gets the list of local variables used by list.tmpl."""
431    checkpoint.checkpoint('Starting')
432    machines = state.machines
433    checkpoint.checkpoint('Got my machines')
434    on = {}
435    has_vnc = {}
436    installing = {}
437    xmlist = state.xmlist
438    checkpoint.checkpoint('Got uptimes')
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    checkpoint.checkpoint('Got max mem/disk')
455    defaults = Defaults(max_memory=max_memory,
456                        max_disk=max_disk,
457                        owner=username)
458    checkpoint.checkpoint('Got defaults')
459    def sortkey(machine):
460        return (machine.owner != username, machine.owner, machine.name)
461    machines = sorted(machines, key=sortkey)
462    d = dict(user=username,
463             cant_add_vm=validation.cantAddVm(username, state),
464             max_memory=max_memory,
465             max_disk=max_disk,
466             defaults=defaults,
467             machines=machines,
468             has_vnc=has_vnc,
469             installing=installing)
470    return d
471
472def getHostname(nic):
473    """Find the hostname associated with a NIC.
474
475    XXX this should be merged with the similar logic in DNS and DHCP.
476    """
477    if nic.hostname:
478        hostname = nic.hostname
479    elif nic.machine:
480        hostname = nic.machine.name
481    else:
482        return None
483    if '.' in hostname:
484        return hostname
485    else:
486        return hostname + '.' + config.dns.domains[0]
487
488def getNicInfo(data_dict, machine):
489    """Helper function for info, get data on nics 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_nics'] = len(machine.nics)
495    nic_fields_template = [('nic%s_hostname', 'NIC %s Hostname'),
496                           ('nic%s_mac', 'NIC %s MAC Addr'),
497                           ('nic%s_ip', 'NIC %s IP'),
498                           ]
499    nic_fields = []
500    for i in range(len(machine.nics)):
501        nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
502        data_dict['nic%s_hostname' % i] = getHostname(machine.nics[i])
503        data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
504        data_dict['nic%s_ip' % i] = machine.nics[i].ip
505    if len(machine.nics) == 1:
506        nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
507    return nic_fields
508
509def getDiskInfo(data_dict, machine):
510    """Helper function for info, get data on disks for a machine.
511
512    Modifies data_dict to include the relevant data, and returns a list
513    of (key, name) pairs to display "name: data_dict[key]" to the user.
514    """
515    data_dict['num_disks'] = len(machine.disks)
516    disk_fields_template = [('%s_size', '%s size')]
517    disk_fields = []
518    for disk in machine.disks:
519        name = disk.guest_device_name
520        disk_fields.extend([(x % name, y % name) for x, y in
521                            disk_fields_template])
522        data_dict['%s_size' % name] = "%0.1f GiB" % (disk.size / 1024.)
523    return disk_fields
524
525def modifyDict(username, state, machine_id, fields):
526    """Modify a machine as specified by CGI arguments.
527
528    Return a dict containing the machine that was modified.
529    """
530    olddisk = {}
531    session.begin()
532    try:
533        kws = dict([(kw, fields[kw]) for kw in
534         'owner admin contact name description memory vmtype disksize'.split()
535                    if fields[kw]])
536        kws['machine_id'] = machine_id
537        validate = validation.Validate(username, state, **kws)
538        machine = validate.machine
539        oldname = machine.name
540
541        if hasattr(validate, 'memory'):
542            machine.memory = validate.memory
543
544        if hasattr(validate, 'vmtype'):
545            machine.type = validate.vmtype
546
547        if hasattr(validate, 'disksize'):
548            disksize = validate.disksize
549            disk = machine.disks[0]
550            if disk.size != disksize:
551                olddisk[disk.guest_device_name] = disksize
552                disk.size = disksize
553                session.save_or_update(disk)
554
555        update_acl = False
556        if hasattr(validate, 'owner') and validate.owner != machine.owner:
557            machine.owner = validate.owner
558            update_acl = True
559        if hasattr(validate, 'name'):
560            machine.name = validate.name
561            for n in machine.nics:
562                if n.hostname == oldname:
563                    n.hostname = validate.name
564        if hasattr(validate, 'description'):
565            machine.description = validate.description
566        if hasattr(validate, 'admin') and validate.admin != machine.administrator:
567            machine.administrator = validate.admin
568            update_acl = True
569        if hasattr(validate, 'contact'):
570            machine.contact = validate.contact
571
572        session.save_or_update(machine)
573        if update_acl:
574            cache_acls.refreshMachine(machine)
575        session.commit()
576    except:
577        session.rollback()
578        raise
579    for diskname in olddisk:
580        controls.resizeDisk(oldname, diskname, str(olddisk[diskname]))
581    if hasattr(validate, 'name'):
582        controls.renameMachine(machine, oldname, validate.name)
583    return dict(machine=machine)
584
585def infoDict(username, state, machine):
586    """Get the variables used by info.tmpl."""
587    status = controls.statusInfo(machine)
588    checkpoint.checkpoint('Getting status info')
589    has_vnc = hasVnc(status)
590    if status is None:
591        main_status = dict(name=machine.name,
592                           memory=str(machine.memory))
593        uptime = None
594        cputime = None
595    else:
596        main_status = dict(status[1:])
597        main_status['host'] = controls.listHost(machine)
598        start_time = float(main_status.get('start_time', 0))
599        uptime = datetime.timedelta(seconds=int(time.time()-start_time))
600        cpu_time_float = float(main_status.get('cpu_time', 0))
601        cputime = datetime.timedelta(seconds=int(cpu_time_float))
602    checkpoint.checkpoint('Status')
603    display_fields = [('name', 'Name'),
604                      ('description', 'Description'),
605                      ('owner', 'Owner'),
606                      ('administrator', 'Administrator'),
607                      ('contact', 'Contact'),
608                      ('type', 'Type'),
609                      'NIC_INFO',
610                      ('uptime', 'uptime'),
611                      ('cputime', 'CPU usage'),
612                      ('host', 'Hosted on'),
613                      ('memory', 'RAM'),
614                      'DISK_INFO',
615                      ('state', 'state (xen format)'),
616                      ]
617    fields = []
618    machine_info = {}
619    machine_info['name'] = machine.name
620    machine_info['description'] = machine.description
621    machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
622    machine_info['owner'] = machine.owner
623    machine_info['administrator'] = machine.administrator
624    machine_info['contact'] = machine.contact
625
626    nic_fields = getNicInfo(machine_info, machine)
627    nic_point = display_fields.index('NIC_INFO')
628    display_fields = (display_fields[:nic_point] + nic_fields +
629                      display_fields[nic_point+1:])
630
631    disk_fields = getDiskInfo(machine_info, machine)
632    disk_point = display_fields.index('DISK_INFO')
633    display_fields = (display_fields[:disk_point] + disk_fields +
634                      display_fields[disk_point+1:])
635
636    main_status['memory'] += ' MiB'
637    for field, disp in display_fields:
638        if field in ('uptime', 'cputime') and locals()[field] is not None:
639            fields.append((disp, locals()[field]))
640        elif field in machine_info:
641            fields.append((disp, machine_info[field]))
642        elif field in main_status:
643            fields.append((disp, main_status[field]))
644        else:
645            pass
646            #fields.append((disp, None))
647
648    checkpoint.checkpoint('Got fields')
649
650
651    max_mem = validation.maxMemory(machine.owner, state, machine, False)
652    checkpoint.checkpoint('Got mem')
653    max_disk = validation.maxDisk(machine.owner, machine)
654    defaults = Defaults()
655    for name in 'machine_id name description administrator owner memory contact'.split():
656        if getattr(machine, name):
657            setattr(defaults, name, getattr(machine, name))
658    defaults.type = machine.type.type_id
659    defaults.disk = "%0.2f" % (machine.disks[0].size/1024.)
660    checkpoint.checkpoint('Got defaults')
661    d = dict(user=username,
662             on=status is not None,
663             machine=machine,
664             defaults=defaults,
665             has_vnc=has_vnc,
666             uptime=str(uptime),
667             ram=machine.memory,
668             max_mem=max_mem,
669             max_disk=max_disk,
670             fields = fields)
671    return d
672
673def send_error_mail(subject, body):
674    import subprocess
675
676    to = config.web.errormail
677    mail = """To: %s
678From: root@%s
679Subject: %s
680
681%s
682""" % (to, config.web.hostname, subject, body)
683    p = subprocess.Popen(['/usr/sbin/sendmail', '-f', to, to],
684                         stdin=subprocess.PIPE)
685    p.stdin.write(mail)
686    p.stdin.close()
687    p.wait()
688
689random.seed() #sigh
Note: See TracBrowser for help on using the repository browser.