18 sys.stderr = StringIO.StringIO()
19 sys.path.append('/home/ecprice/.local/lib/python2.5/site-packages')
21 from Cheetah.Template import Template
22 from sipb_xen_database import *
25 class MyException(Exception):
26 """Base class for my exceptions"""
29 class InvalidInput(MyException):
30 """Exception for user-provided input is invalid but maybe in good faith.
32 This would include setting memory to negative (which might be a
33 typo) but not setting an invalid boot CD (which requires bypassing
36 def __init__(self, err_field, err_value, expl=None):
37 MyException.__init__(self, expl)
38 self.err_field = err_field
39 self.err_value = err_value
41 class CodeError(MyException):
42 """Exception for internal errors or bad faith input."""
46 def __init__(self, user):
49 def __get_uptimes(self):
50 if not hasattr(self, '_uptimes'):
51 self._uptimes = getUptimes(Machine.select())
53 uptimes = property(__get_uptimes)
58 """Return HTML code for a (?) link to a specified help topic"""
59 return '<span class="helplink"><a href="help?subject='+subj+'&simple=true" target="_blank" onclick="return helppopup(\''+subj+'\')">(?)</a></span>'
63 global_dict['helppopup'] = helppopup
66 # ... and stolen from xend/uuid.py
68 """Generate a random UUID."""
70 return [ random.randint(0, 255) for _ in range(0, 16) ]
73 """Turn a numeric UUID to a hyphen-seperated one."""
74 return "-".join(["%02x" * 4, "%02x" * 2, "%02x" * 2, "%02x" * 2,
75 "%02x" * 6]) % tuple(u)
77 MAX_MEMORY_TOTAL = 512
78 MAX_MEMORY_SINGLE = 256
79 MIN_MEMORY_SINGLE = 16
86 def getMachinesByOwner(owner):
87 """Return the machines owned by a given owner."""
88 return Machine.select_by(owner=owner)
90 def maxMemory(user, machine=None, on=True):
91 """Return the maximum memory for a machine or a user.
93 If machine is None, return the memory available for a new
94 machine. Else, return the maximum that machine can have.
96 on is whether the machine should be turned on. If false, the max
97 memory for the machine to change to, if it is left off, is
101 return MAX_MEMORY_SINGLE
102 machines = getMachinesByOwner(user.username)
103 active_machines = [x for x in machines if g.uptimes[x]]
104 mem_usage = sum([x.memory for x in active_machines if x != machine])
105 return min(MAX_MEMORY_SINGLE, MAX_MEMORY_TOTAL-mem_usage)
107 def maxDisk(user, machine=None):
108 machines = getMachinesByOwner(user.username)
109 disk_usage = sum([sum([y.size for y in x.disks])
110 for x in machines if x != machine])
111 return min(MAX_DISK_SINGLE, MAX_DISK_TOTAL-disk_usage/1024.)
114 machines = getMachinesByOwner(user.username)
115 active_machines = [x for x in machines if g.uptimes[x]]
116 return (len(machines) < MAX_VMS_TOTAL and
117 len(active_machines) < MAX_VMS_ACTIVE)
119 def haveAccess(user, machine):
120 """Return whether a user has access to a machine"""
121 if user.username == 'moo':
123 return getafsgroups.checkLockerOwner(user.username, machine.owner)
125 def error(op, user, fields, err, emsg):
126 """Print an error page when a CodeError occurs"""
127 d = dict(op=op, user=user, errorMessage=str(err),
129 return Template(file='error.tmpl', searchList=[d, global_dict]);
131 def invalidInput(op, user, fields, err, emsg):
132 """Print an error page when an InvalidInput exception occurs"""
133 d = dict(op=op, user=user, err_field=err.err_field,
134 err_value=str(err.err_value), stderr=emsg,
135 errorMessage=str(err))
136 return Template(file='invalid.tmpl', searchList=[d, global_dict]);
138 def validMachineName(name):
139 """Check that name is valid for a machine name"""
142 charset = string.ascii_letters + string.digits + '-_'
143 if name[0] in '-_' or len(name) > 22:
150 def kinit(username = 'tabbott/extra', keytab = '/etc/tabbott.keytab'):
151 """Kinit with a given username and keytab"""
153 p = subprocess.Popen(['kinit', "-k", "-t", keytab, username],
154 stderr=subprocess.PIPE)
157 raise CodeError("Error %s in kinit: %s" % (e, p.stderr.read()))
160 """If we lack tickets, kinit."""
161 p = subprocess.Popen(['klist', '-s'])
165 def remctl(*args, **kws):
166 """Perform a remctl and return the output.
168 kinits if necessary, and outputs errors to stderr.
171 p = subprocess.Popen(['remctl', 'black-mesa.mit.edu']
173 stdout=subprocess.PIPE,
174 stderr=subprocess.PIPE)
177 return p.stdout.read(), p.stderr.read()
179 print >> sys.stderr, 'Error on remctl %s:' % args
180 print >> sys.stderr, p.stderr.read()
181 raise CodeError('ERROR on remctl')
182 return p.stdout.read()
184 def lvcreate(machine, disk):
185 """Create a single disk for a machine"""
186 remctl('web', 'lvcreate', machine.name,
187 disk.guest_device_name, str(disk.size))
189 def makeDisks(machine):
190 """Update the lvm partitions to add a disk."""
191 for disk in machine.disks:
192 lvcreate(machine, disk)
194 def bootMachine(machine, cdtype):
195 """Boot a machine with a given boot CD.
197 If cdtype is None, give no boot cd. Otherwise, it is the string
198 id of the CD (e.g. 'gutsy_i386')
200 if cdtype is not None:
201 remctl('web', 'vmboot', machine.name,
204 remctl('web', 'vmboot', machine.name)
206 def registerMachine(machine):
207 """Register a machine to be controlled by the web interface"""
208 remctl('web', 'register', machine.name)
210 def unregisterMachine(machine):
211 """Unregister a machine to not be controlled by the web interface"""
212 remctl('web', 'unregister', machine.name)
215 """Parse a status string into nested tuples of strings.
217 s = output of xm list --long <machine_name>
219 values = re.split('([()])', s)
221 for v in values[2:-2]: #remove initial and final '()'
228 if len(stack[-1]) == 1:
230 stack[-2].append(stack[-1])
235 stack[-1].extend(v.split())
238 def getUptimes(machines=None):
239 """Return a dictionary mapping machine names to uptime strings"""
240 value_string = remctl('web', 'listvms')
241 lines = value_string.splitlines()
246 uptime = ' '.join(lst[2:])
250 ans[m] = d.get(m.name)
253 def statusInfo(machine):
254 """Return the status list for a given machine.
256 Gets and parses xm list --long
258 value_string, err_string = remctl('list-long', machine.name, err=True)
259 if 'Unknown command' in err_string:
260 raise CodeError("ERROR in remctl list-long %s is not registered" % (machine.name,))
261 elif 'does not exist' in err_string:
264 raise CodeError("ERROR in remctl list-long %s: %s" % (machine.name, err_string))
265 status = parseStatus(value_string)
269 """Does the machine with a given status list support VNC?"""
273 if l[0] == 'device' and l[1][0] == 'vfb':
275 return 'location' in d
278 def createVm(user, name, memory, disk, is_hvm, cdrom):
279 """Create a VM and put it in the database"""
280 # put stuff in the table
281 transaction = ctx.current.create_transaction()
283 if memory > maxMemory(user):
284 raise InvalidInput('memory', memory,
285 "Max %s" % maxMemory(user))
286 if disk > maxDisk(user) * 1024:
287 raise InvalidInput('disk', disk,
288 "Max %s" % maxDisk(user))
289 if not canAddVm(user):
290 raise InvalidInput('create', True, 'Unable to create more VMs')
291 res = meta.engine.execute('select nextval(\'"machines_machine_id_seq"\')')
292 id = res.fetchone()[0]
294 machine.machine_id = id
296 machine.memory = memory
297 machine.owner = user.username
298 machine.contact = user.email
299 machine.uuid = uuidToString(randomUUID())
300 machine.boot_off_cd = True
301 machine_type = Type.get_by(hvm=is_hvm)
302 machine.type_id = machine_type.type_id
303 ctx.current.save(machine)
304 disk = Disk(machine.machine_id,
306 open = NIC.select_by(machine_id=None)
307 if not open: #No IPs left!
308 raise CodeError("No IP addresses left! Contact sipb-xen-dev@mit.edu")
310 nic.machine_id = machine.machine_id
312 ctx.current.save(nic)
313 ctx.current.save(disk)
316 transaction.rollback()
318 registerMachine(machine)
320 # tell it to boot with cdrom
321 bootMachine(machine, cdrom)
325 def validMemory(user, memory, machine=None, on=True):
326 """Parse and validate limits for memory for a given user and machine.
328 on is whether the memory must be valid after the machine is
333 if memory < MIN_MEMORY_SINGLE:
336 raise InvalidInput('memory', memory,
337 "Minimum %s MB" % MIN_MEMORY_SINGLE)
338 if memory > maxMemory(user, machine, on):
339 raise InvalidInput('memory', memory,
340 'Maximum %s MB' % maxMemory(user, machine))
343 def validDisk(user, disk, machine=None):
344 """Parse and validate limits for disk for a given user and machine."""
347 if disk > maxDisk(user, machine):
348 raise InvalidInput('disk', disk,
349 "Maximum %s G" % maxDisk(user, machine))
350 disk = int(disk * 1024)
351 if disk < MIN_DISK_SINGLE * 1024:
354 raise InvalidInput('disk', disk,
355 "Minimum %s GB" % MIN_DISK_SINGLE)
358 def create(user, fields):
359 """Handler for create requests."""
360 name = fields.getfirst('name')
361 if not validMachineName(name):
362 raise InvalidInput('name', name)
365 if Machine.get_by(name=name):
366 raise InvalidInput('name', name,
369 memory = fields.getfirst('memory')
370 memory = validMemory(user, memory, on=True)
372 disk = fields.getfirst('disk')
373 disk = validDisk(user, disk)
375 vm_type = fields.getfirst('vmtype')
376 if vm_type not in ('hvm', 'paravm'):
377 raise CodeError("Invalid vm type '%s'" % vm_type)
378 is_hvm = (vm_type == 'hvm')
380 cdrom = fields.getfirst('cdrom')
381 if cdrom is not None and not CDROM.get(cdrom):
382 raise CodeError("Invalid cdrom type '%s'" % cdrom)
384 machine = createVm(user, name, memory, disk, is_hvm, cdrom)
387 return Template(file='create.tmpl',
388 searchList=[d, global_dict]);
390 def listVms(user, fields):
391 """Handler for list requests."""
392 machines = [m for m in Machine.select() if haveAccess(user, m)]
402 has_vnc[m] = "ParaVM"+helppopup("paravm_console")
404 # status = statusInfo(m)
405 # on[m.name] = status is not None
406 # has_vnc[m.name] = hasVnc(status)
407 max_mem=maxMemory(user)
408 max_disk=maxDisk(user)
410 can_add_vm=canAddVm(user),
414 default_disk=min(4.0, max_disk),
418 cdroms=CDROM.select())
419 return Template(file='list.tmpl', searchList=[d, global_dict])
421 def testMachineId(user, machineId, exists=True):
422 """Parse, validate and check authorization for a given machineId.
424 If exists is False, don't check that it exists.
426 if machineId is None:
427 raise CodeError("No machine ID specified")
429 machineId = int(machineId)
431 raise CodeError("Invalid machine ID '%s'" % machineId)
432 machine = Machine.get(machineId)
433 if exists and machine is None:
434 raise CodeError("No such machine ID '%s'" % machineId)
435 if machine is not None and not haveAccess(user, machine):
436 raise CodeError("No access to machine ID '%s'" % machineId)
439 def vnc(user, fields):
442 Note that due to same-domain restrictions, the applet connects to
443 the webserver, which needs to forward those requests to the xen
444 server. The Xen server runs another proxy that (1) authenticates
445 and (2) finds the correct port for the VM.
447 You might want iptables like:
449 -t nat -A PREROUTING -s ! 18.181.0.60 -i eth1 -p tcp -m tcp --dport 10003 -j DNAT --to-destination 18.181.0.60:10003
450 -t nat -A POSTROUTING -d 18.181.0.60 -o eth1 -p tcp -m tcp --dport 10003 -j SNAT --to-source 18.187.7.142
451 -A FORWARD -d 18.181.0.60 -i eth1 -o eth1 -p tcp -m tcp --dport 10003 -j ACCEPT
453 Remember to enable iptables!
454 echo 1 > /proc/sys/net/ipv4/ip_forward
456 machine = testMachineId(user, fields.getfirst('machine_id'))
458 TOKEN_KEY = "0M6W0U1IXexThi5idy8mnkqPKEq1LtEnlK/pZSn0cDrN"
461 data["user"] = user.username
462 data["machine"]=machine.name
463 data["expires"]=time.time()+(5*60)
464 pickledData = cPickle.dumps(data)
465 m = hmac.new(TOKEN_KEY, digestmod=sha)
466 m.update(pickledData)
467 token = {'data': pickledData, 'digest': m.digest()}
468 token = cPickle.dumps(token)
469 token = base64.urlsafe_b64encode(token)
471 status = statusInfo(machine)
472 has_vnc = hasVnc(status)
478 hostname=os.environ.get('SERVER_NAME', 'localhost'),
480 return Template(file='vnc.tmpl',
481 searchList=[d, global_dict])
483 def getNicInfo(data_dict, machine):
484 """Helper function for info, get data on nics for a machine.
486 Modifies data_dict to include the relevant data, and returns a list
487 of (key, name) pairs to display "name: data_dict[key]" to the user.
489 data_dict['num_nics'] = len(machine.nics)
490 nic_fields_template = [('nic%s_hostname', 'NIC %s hostname'),
491 ('nic%s_mac', 'NIC %s MAC Addr'),
492 ('nic%s_ip', 'NIC %s IP'),
495 for i in range(len(machine.nics)):
496 nic_fields.extend([(x % i, y % i) for x, y in nic_fields_template])
497 data_dict['nic%s_hostname' % i] = machine.nics[i].hostname + '.servers.csail.mit.edu'
498 data_dict['nic%s_mac' % i] = machine.nics[i].mac_addr
499 data_dict['nic%s_ip' % i] = machine.nics[i].ip
500 if len(machine.nics) == 1:
501 nic_fields = [(x, y.replace('NIC 0 ', '')) for x, y in nic_fields]
504 def getDiskInfo(data_dict, machine):
505 """Helper function for info, get data on disks for a machine.
507 Modifies data_dict to include the relevant data, and returns a list
508 of (key, name) pairs to display "name: data_dict[key]" to the user.
510 data_dict['num_disks'] = len(machine.disks)
511 disk_fields_template = [('%s_size', '%s size')]
513 for disk in machine.disks:
514 name = disk.guest_device_name
515 disk_fields.extend([(x % name, y % name) for x, y in disk_fields_template])
516 data_dict['%s_size' % name] = "%0.1f GB" % (disk.size / 1024.)
519 def deleteVM(machine):
521 remctl('destroy', machine.name, err=True)
522 transaction = ctx.current.create_transaction()
523 delete_disk_pairs = [(machine.name, d.guest_device_name) for d in machine.disks]
525 for nic in machine.nics:
526 nic.machine_id = None
528 ctx.current.save(nic)
529 for disk in machine.disks:
530 ctx.current.delete(disk)
531 ctx.current.delete(machine)
534 transaction.rollback()
536 for mname, dname in delete_disk_pairs:
537 remctl('web', 'lvremove', mname, dname)
538 unregisterMachine(machine)
540 def command(user, fields):
541 """Handler for running commands like boot and delete on a VM."""
542 print >> sys.stderr, time.time()-start_time
543 machine = testMachineId(user, fields.getfirst('machine_id'))
544 action = fields.getfirst('action')
545 cdrom = fields.getfirst('cdrom')
546 print >> sys.stderr, time.time()-start_time
547 if cdrom is not None and not CDROM.get(cdrom):
548 raise CodeError("Invalid cdrom type '%s'" % cdrom)
549 if action not in ('Reboot', 'Power on', 'Power off', 'Shutdown', 'Delete VM'):
550 raise CodeError("Invalid action '%s'" % action)
551 if action == 'Reboot':
552 if cdrom is not None:
553 remctl('reboot', machine.name, cdrom)
555 remctl('reboot', machine.name)
556 elif action == 'Power on':
557 if maxMemory(user) < machine.memory:
558 raise InvalidInput('action', 'Power on',
559 "You don't have enough free RAM quota to turn on this machine")
560 bootMachine(machine, cdrom)
561 elif action == 'Power off':
562 remctl('destroy', machine.name)
563 elif action == 'Shutdown':
564 remctl('shutdown', machine.name)
565 elif action == 'Delete VM':
567 print >> sys.stderr, time.time()-start_time
572 return Template(file="command.tmpl", searchList=[d, global_dict])
574 def testOwner(user, owner, machine=None):
575 if owner == machine.owner: #XXX What do we do when you lose access to the locker?
577 value = getafsgroups.checkLockerOwner(user.username, owner, verbose=True)
580 raise InvalidInput('owner', owner, value)
582 def testContact(user, contact, machine=None):
583 if not re.match("^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$", contact, re.I):
584 raise InvalidInput('contact', contact, "Not a valid email")
587 def testDisk(user, disksize, machine=None):
590 def testName(user, name, machine=None):
593 if not Machine.select_by(name=name):
595 if name == machine.name:
597 raise InvalidInput('name', name, "Already taken")
599 def testHostname(user, hostname, machine):
600 for nic in machine.nics:
601 if hostname == nic.hostname:
603 # check if doesn't already exist
604 if NIC.select_by(hostname=hostname):
605 raise InvalidInput('hostname', hostname,
607 if not re.match("^[A-Z0-9-]{1,22}$", hostname, re.I):
608 raise InvalidInput('hostname', hostname, "Not a valid hostname; must only use number, letters, and dashes.")
611 def modify(user, fields):
612 """Handler for modifying attributes of a machine."""
615 transaction = ctx.current.create_transaction()
617 machine = testMachineId(user, fields.getfirst('machine_id'))
618 owner = testOwner(user, fields.getfirst('owner'), machine)
619 contact = testContact(user, fields.getfirst('contact'))
620 hostname = testHostname(owner, fields.getfirst('hostname'),
622 name = testName(user, fields.getfirst('name'), machine)
623 oldname = machine.name
626 memory = fields.getfirst('memory')
627 if memory is not None:
628 memory = validMemory(user, memory, machine, on=False)
629 machine.memory = memory
631 disksize = testDisk(user, fields.getfirst('disk'))
632 if disksize is not None:
633 disksize = validDisk(user, disksize, machine)
634 disk = machine.disks[0]
635 if disk.size != disksize:
636 olddisk[disk.guest_device_name] = disksize
638 ctx.current.save(disk)
640 # XXX first NIC gets hostname on change? Interface doesn't support more.
641 for nic in machine.nics[:1]:
642 nic.hostname = hostname
643 ctx.current.save(nic)
645 if owner is not None and owner != machine.owner:
646 machine.owner = owner
647 if name is not None and name != machine.name:
650 ctx.current.save(machine)
653 transaction.rollback()
655 for diskname in olddisk:
656 remctl("web", "lvresize", oldname, diskname, str(olddisk[diskname]))
657 if name is not None and name != oldname:
658 for disk in machine.disks:
660 remctl("web", "lvrename", oldname, disk.guest_device_name, name)
661 remctl("web", "moveregister", oldname, name)
665 return Template(file="command.tmpl", searchList=[d, global_dict])
668 def help(user, fields):
669 """Handler for help messages."""
670 simple = fields.getfirst('simple')
671 subjects = fields.getlist('subject')
673 mapping = dict(paravm_console="""
674 ParaVM machines do not support console access over VNC. To access
675 these machines, you either need to boot with a liveCD and ssh in or
676 hope that the sipb-xen maintainers add support for serial consoles.""",
678 HVM machines use the virtualization features of the processor, while
679 ParaVM machines use Xen's emulation of virtualization features. You
680 want an HVM virtualized machine.""",
681 cpu_weight="""Don't ask us! We're as mystified as you are.""",
682 owner="""The Owner must be the name of a locker that you are an AFS
683 administrator of. In particular, you or an AFS group you are a member
684 of must have AFS rlidwka bits on the locker. You can check see who
685 administers the LOCKER locker using the command 'fs la /mit/LOCKER' on
693 return Template(file="help.tmpl", searchList=[d, global_dict])
696 def info(user, fields):
697 """Handler for info on a single VM."""
698 machine = testMachineId(user, fields.getfirst('machine_id'))
699 status = statusInfo(machine)
700 has_vnc = hasVnc(status)
702 main_status = dict(name=machine.name,
703 memory=str(machine.memory))
707 main_status = dict(status[1:])
708 start_time = float(main_status.get('start_time', 0))
709 uptime = datetime.timedelta(seconds=int(time.time()-start_time))
710 cpu_time_float = float(main_status.get('cpu_time', 0))
711 cputime = datetime.timedelta(seconds=int(cpu_time_float))
712 display_fields = """name uptime memory state cpu_weight on_reboot
713 on_poweroff on_crash on_xend_start on_xend_stop bootloader""".split()
714 display_fields = [('name', 'Name'),
716 ('contact', 'Contact'),
719 ('uptime', 'uptime'),
720 ('cputime', 'CPU usage'),
723 ('state', 'state (xen format)'),
724 ('cpu_weight', 'CPU weight'+helppopup('cpu_weight')),
725 ('on_reboot', 'Action on VM reboot'),
726 ('on_poweroff', 'Action on VM poweroff'),
727 ('on_crash', 'Action on VM crash'),
728 ('on_xend_start', 'Action on Xen start'),
729 ('on_xend_stop', 'Action on Xen stop'),
730 ('bootloader', 'Bootloader options'),
734 machine_info['name'] = machine.name
735 machine_info['type'] = machine.type.hvm and 'HVM' or 'ParaVM'
736 machine_info['owner'] = machine.owner
737 machine_info['contact'] = machine.contact
739 nic_fields = getNicInfo(machine_info, machine)
740 nic_point = display_fields.index('NIC_INFO')
741 display_fields = display_fields[:nic_point] + nic_fields + display_fields[nic_point+1:]
743 disk_fields = getDiskInfo(machine_info, machine)
744 disk_point = display_fields.index('DISK_INFO')
745 display_fields = display_fields[:disk_point] + disk_fields + display_fields[disk_point+1:]
747 main_status['memory'] += ' MB'
748 for field, disp in display_fields:
749 if field in ('uptime', 'cputime') and locals()[field] is not None:
750 fields.append((disp, locals()[field]))
751 elif field in machine_info:
752 fields.append((disp, machine_info[field]))
753 elif field in main_status:
754 fields.append((disp, main_status[field]))
757 #fields.append((disp, None))
758 max_mem = maxMemory(user, machine)
759 max_disk = maxDisk(user, machine)
761 cdroms=CDROM.select(),
762 on=status is not None,
769 owner_help=helppopup("owner"),
771 return Template(file='info.tmpl',
772 searchList=[d, global_dict])
774 mapping = dict(list=listVms,
782 if __name__ == '__main__':
783 start_time = time.time()
784 fields = cgi.FieldStorage()
787 email = 'moo@cow.com'
790 if 'SSL_CLIENT_S_DN_Email' in os.environ:
791 username = os.environ[ 'SSL_CLIENT_S_DN_Email'].split("@")[0]
792 u.username = username
793 u.email = os.environ[ 'SSL_CLIENT_S_DN_Email']
797 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
798 operation = os.environ.get('PATH_INFO', '')
799 # print 'Content-Type: text/plain\n'
802 print "Status: 301 Moved Permanently"
803 print 'Location: ' + os.environ['SCRIPT_NAME']+'/\n'
806 if operation.startswith('/'):
807 operation = operation[1:]
811 def badOperation(u, e):
812 raise CodeError("Unknown operation")
814 fun = mapping.get(operation, badOperation)
815 if fun not in (help, ):
816 connect('postgres://sipb-xen@sipb-xen-dev/sipb_xen')
818 output = fun(u, fields)
819 print 'Content-Type: text/html\n'
821 e = sys.stderr.read()
822 sys.stderr=sys.stdout
825 output = output.replace('<body>', '<body><p>STDERR:</p><pre>'+e+'</pre>')
827 except CodeError, err:
828 print 'Content-Type: text/html\n'
830 e = sys.stderr.read()
831 sys.stderr=sys.stdout
832 print error(operation, u, fields, err, e)
833 except InvalidInput, err:
834 print 'Content-Type: text/html\n'
836 e = sys.stderr.read()
837 sys.stderr=sys.stdout
838 print invalidInput(operation, u, fields, err, e)
840 print 'Content-Type: text/plain\n'
842 e = sys.stderr.read()
845 sys.stderr = sys.stdout