# Copyright (C) 2007, Pascal Scheffers <pascal@scheffers.net>
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
# OTHER DEALINGS IN THE SOFTWARE.
#
# log-collect for OLPC
#
# Compile a report containing:
# * Basic system information:
# ** Serial number
# ** Battery type
# ** Build number
# ** Uptime
# ** disk free space
# ** ...
# * Installed packages list
# * All relevant log files (all of them, at first)
#
# The report is output as a tarfile
#
# This file has two modes:
# 1. It is a stand-alone python script, when invoked as 'log-collect'
# 2. It is a python module.
import os
import zipfile
import glob
import sys
import time
# The next couple are used by LogSend
import httplib
import mimetypes
import urlparse
MFG_DATA_PATHS = ['/ofw/mfg-data/', '/proc/device-tree/mfg-data/']
class MachineProperties:
"""Various machine properties in easy to access chunks.
"""
def __read_file(self, filename):
"""Read the entire contents of a file and return it as a string"""
data = ''
f = open(filename)
try:
data = f.read()
finally:
f.close()
return data
def olpc_build(self):
"""Buildnumber, from /etc/issue"""
# Is there a better place to get the build number?
if not os.path.exists('/etc/issue'):
return '#/etc/issue not found'
# Needed, because we want to default to the first non blank line:
first_line = ''
for line in self.__read_file('/etc/issue').splitlines():
if line.lower().find('olpc build') > -1:
return line
if first_line == '':
first_line=line
return first_line
def uptime(self):
for line in self.__read_file('/proc/uptime').splitlines():
if line != '':
return line
return ''
def loadavg(self):
for line in self.__read_file('/proc/loadavg').splitlines():
if line != '':
return line
return ''
def kernel_version(self):
for line in self.__read_file('/proc/version').splitlines():
if line != '':
return line
return ''
def memfree(self):
line = ''
for line in self.__read_file('/proc/meminfo').splitlines():
if line.find('MemFree:') > -1:
return line[8:].strip()
def _trim_null(self, v):
if v != '' and ord(v[len(v)-1]) == 0:
v = v[:len(v)-1]
return v
def _mfg_data(self, item):
"""Return mfg data item from mfg-data directory"""
mfg_path = None
for test_path in MFG_DATA_PATHS:
if os.path.exists(test_path + item):
mfg_path = test_path + item
break
if mfg_path == None:
return ''
v = self._trim_null(self.__read_file(mfg_path))
return v
def laptop_serial_number(self):
return self._mfg_data('SN')
def laptop_motherboard_number(self):
return self._mfg_data('B#')
def laptop_board_revision(self):
s = self._mfg_data('SG')[0:1]
if s == '':
return ''
return '%02X' % ord(self._mfg_data('SG')[0:1])
def laptop_uuid(self):
return self._mfg_data('U#')
def laptop_keyboard(self):
kb = self._mfg_data('KM') + '-'
kb += self._mfg_data('KL') + '-'
kb += self._mfg_data('KV')
return kb
def laptop_wireless_mac(self):
return self._mfg_data('WM')
def laptop_bios_version(self):
try:
d = open('/proc/device-tree/openprom/model', 'r').read()
v = self._trim_null(d)
return v
except:
pass
try:
d = open('/ofw/openprom/model', 'r').read()
v = self._trim_null(d)
return v
except:
pass
try:
d = open('/sys/class/dmi/id/bios_version', 'r').read()
v = self._trim_null(d)
return v
except:
pass
return ''
def laptop_country(self):
return self._mfg_data('LA')
def laptop_localization(self):
return self._mfg_data('LO')
def _battery_info(self, item):
""" from /sys/class/power-supply/olpc-battery/ """
root = '/sys/class/power_supply/olpc-battery/'
if not os.path.exists(root+item):
return ''
return self.__read_file(root+item).strip()
def battery_serial_number(self):
return self._battery_info('serial_number')
def battery_capacity(self):
return self._battery_info('capacity') + ' ' + \
self._battery_info('capacity_level')
def battery_info(self):
#Should be just:
#return self._battery_info('uevent')
#But because of a bug in the kernel, that has trash, lets filter:
bi = ''
for line in self._battery_info('uevent').splitlines():
if line.startswith('POWER_'):
bi += line + '\n'
return bi
def disksize(self, path):
return os.statvfs(path).f_bsize * os.statvfs(path).f_blocks
def diskfree(self, path):
return os.statvfs(path).f_bsize * os.statvfs(path).f_bavail
def _read_popen(self, cmd):
p = os.popen(cmd)
s = ''
try:
for line in p:
s += line
finally:
p.close()
return s
def ifconfig(self):
return self._read_popen('/sbin/ifconfig')
def route_n(self):
return self._read_popen('/sbin/route -n')
def df_a(self):
return self._read_popen('/bin/df -a')
def ps_auxfwww(self):
return self._read_popen('/bin/ps auxfwww')
def usr_bin_free(self):
return self._read_popen('/usr/bin/free')
def top(self):
return self._read_popen('/usr/bin/top -bn2')
def installed_activities(self):
s = ''
for path in glob.glob('/usr/share/sugar/activities/*.activity'):
s += os.path.basename(path) + '\n'
home = os.path.expanduser('~')
for path in glob.glob(os.path.join(home, 'Activities', '*')):
s += '~' + os.path.basename(path) + '\n'
return s
class LogCollect:
"""Collect XO logfiles and machine metadata for reporting to OLPC
"""
def __init__(self):
self._mp = MachineProperties()
def write_logs(self, archive='', logbytes=15360):
"""Write a zipfile containing the tails of the logfiles and machine info of the XO
Arguments:
archive - Specifies the location where to store the data
defaults to /dev/shm/logs-<xo-serial>.zip
logbytes - Maximum number of bytes to read from each log file.
0 means complete logfiles, not just the tail
-1 means only save machine info, no logs
"""
#This function is crammed with try...except to make sure we get as much
#data as possible, if anything fails.
if archive=='':
archive = '/dev/shm/logs.zip'
try:
#With serial number is more convenient, but might fail for some
#Unknown reason...
archive = '/dev/shm/logs-%s.zip' % self._mp.laptop_serial_number()
except Exception:
pass
z = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED)
try:
try:
z.writestr('info.txt', self.laptop_info())
except Exception, e:
z.writestr('info.txt',
"logcollect: could not add info.txt: %s" % e)
if logbytes > -1:
# Include some log files from /var/log.
for fn in ['dmesg', 'messages', 'cron', 'maillog','rpmpkgs',
'Xorg.0.log', 'spooler']:
try:
if os.access('/var/log/'+fn, os.F_OK):
if logbytes == 0:
z.write('/var/log/'+fn, 'var-log/'+fn)
else:
z.writestr('var-log/'+fn,
self.file_tail('/var/log/'+fn, logbytes))
except Exception, e:
z.writestr('var-log/'+fn,
"logcollect: could not add %s: %s" % (fn, e))
home = os.path.expanduser('~')
here = os.path.join(home, '.sugar/default/logs/*.log')
for path in glob.glob(here):
if os.access(path, os.F_OK):
pref = 'sugar-logs/'
name = os.path.join(pref, os.path.basename(path))
try:
if logbytes == 0:
z.write(path, name)
else:
z.writestr(name,
self.file_tail(path, logbytes))
except Exception, e:
z.writestr(name,
"logcollect: could not add %s: %s" % (name, e))
here = os.path.join(home, '.sugar/default/logs/*/*.log')
for path in glob.glob(here):
if os.access(path, os.F_OK):
when = os.path.basename(os.path.dirname(path))
pref = 'sugar-logs-%s/' % when
name = os.path.join(pref, os.path.basename(path))
try:
if logbytes == 0:
z.write(path, name)
else:
z.writestr(name,
self.file_tail(path, logbytes))
except Exception, e:
z.writestr(name,
"logcollect: could not add %s: %s" % (name, e))
try:
z.write('/etc/resolv.conf')
except Exception, e:
z.writestr('/etc/resolv.conf',
"logcollect: could not add resolv.conf: %s" % e)
except Exception, e:
print 'While creating zip archive: %s' % e
z.close()
return archive
def file_tail(self, filename, tailbytes):
"""Read the tail (end) of the file
Arguments:
filename The name of the file to read
tailbytes Number of bytes to include or 0 for entire file
"""
data = ''
f = open(filename)
try:
fsize = os.stat(filename).st_size
if tailbytes > 0 and fsize > tailbytes:
f.seek(-tailbytes, 2)
data = f.read()
finally:
f.close()
return data
def make_report(self, target='stdout'):
"""Create the report
Arguments:
target - where to save the logs, a path or stdout
"""
li = self.laptop_info()
for k, v in li.iteritems():
print k + ': ' +v
print self._mp.battery_info()
def laptop_info(self):
"""Return a string with laptop serial, battery type, build, memory info, etc."""
s = ''
try:
# Do not include UUID!
s += 'laptop-info-version: 1.0\n'
s += 'clock: %f\n' % time.clock()
s += 'date: %s\n' % time.strftime("%a, %d %b %Y %H:%M:%S +0000",
time.gmtime())
s += 'memfree: %s\n' % self._mp.memfree()
s += 'disksize: %s MB\n' % ( self._mp.disksize('/') / (1024*1024) )
s += 'diskfree: %s MB\n' % ( self._mp.diskfree('/') / (1024*1024) )
s += 'olpc_build: %s\n' % self._mp.olpc_build()
s += 'kernel_version: %s\n' % self._mp.kernel_version()
s += 'uptime: %s\n' % self._mp.uptime()
s += 'loadavg: %s\n' % self._mp.loadavg()
s += 'serial-number: %s\n' % self._mp.laptop_serial_number()
s += 'motherboard-number: %s\n' % self._mp.laptop_motherboard_number()
s += 'board-revision: %s\n' % self._mp.laptop_board_revision()
s += 'keyboard: %s\n' % self._mp.laptop_keyboard()
s += 'wireless_mac: %s\n' % self._mp.laptop_wireless_mac()
s += 'firmware: %s\n' % self._mp.laptop_bios_version()
s += 'country: %s\n' % self._mp.laptop_country()
s += 'localization: %s\n' % self._mp.laptop_localization()
s += self._mp.battery_info()
s += "\n[/sbin/ifconfig]\n%s\n" % self._mp.ifconfig()
s += "\n[/sbin/route -n]\n%s\n" % self._mp.route_n()
s += '\n[Installed Activities]\n%s\n' % self._mp.installed_activities()
s += '\n[df -a]\n%s\n' % self._mp.df_a()
s += '\n[ps auxwww]\n%s\n' % self._mp.ps_auxfwww()
s += '\n[free]\n%s\n' % self._mp.usr_bin_free()
s += '\n[top -bn2]\n%s\n' % self._mp.top()
except Exception, e:
s += '\nException while building info:\n%s\n' % e
return s
class LogSend:
# post_multipart and encode_multipart_formdata have been taken from
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/146306
def post_multipart(self, host, selector, fields, files):
"""
Post fields and files to an http host as multipart/form-data.
fields is a sequence of (name, value) elements for regular form fields.
files is a sequence of (name, filename, value) elements for data to be uploaded as files
Return the server's response page.
"""
content_type, body = self.encode_multipart_formdata(fields, files)
h = httplib.HTTP(host)
h.putrequest('POST', selector)
h.putheader('content-type', content_type)
h.putheader('content-length', str(len(body)))
h.putheader('Host', host)
h.endheaders()
h.send(body)
errcode, errmsg, headers = h.getreply()
return h.file.read()
def encode_multipart_formdata(self, fields, files):
"""
fields is a sequence of (name, value) elements for regular form fields.
files is a sequence of (name, filename, value) elements for data to be uploaded as files
Return (content_type, body) ready for httplib.HTTP instance
"""
BOUNDARY = '----------ThIs_Is_tHe_bouNdaRY_$'
CRLF = '\r\n'
L = []
for (key, value) in fields:
L.append('--' + BOUNDARY)
L.append('Content-Disposition: form-data; name="%s"' % key)
L.append('')
L.append(value)
for (key, filename, value) in files:
L.append('--' + BOUNDARY)
L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % (key, filename))
L.append('Content-Type: %s' % self.get_content_type(filename))
L.append('')
L.append(value)
L.append('--' + BOUNDARY + '--')
L.append('')
body = CRLF.join(L)
content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
return content_type, body
def read_file(self, filename):
"""Read the entire contents of a file and return it as a string"""
data = ''
f = open(filename)
try:
data = f.read()
finally:
f.close()
return data
def get_content_type(self, filename):
return mimetypes.guess_type(filename)[0] or 'application/octet-stream'
def http_post_logs(self, url, archive):
#host, selector, fields, files
files = ('logs', os.path.basename(archive), self.read_file(archive)),
# Client= olpc will make the server return just "OK" or "FAIL"
fields = ('client', 'xo'),
urlparts = urlparse.urlsplit(url)
print "Sending logs to %s" % url
r = self.post_multipart(urlparts[1], urlparts[2], fields, files)
print r
return (r == 'OK')
# This script is dual-mode, it can be used as a command line tool and as
# a library.
if sys.argv[0].endswith('logcollect.py') or \
sys.argv[0].endswith('logcollect'):
print 'log-collect utility 1.0'
lc = LogCollect()
ls = LogSend()
logs = ''
mode = 'http'
if len(sys.argv)==1:
print """logcollect.py - send your XO logs to OLPC
Usage:
logcollect.py http://server.name/submit.php
- submit logs to a server
logcollect.py file:/media/xxxx-yyyy/mylog.zip
- save the zip file on a USB device or SD card
logcollect.py all file:/media/xxxx-yyyy/mylog.zip
- Save to zip file and include ALL logs
logcollect.py none http
- Just send info.txt, but no logs via http.
logcollect.py none file
- Just save info.txt in /dev/shm/logs-SN123.zip
If you specify 'all' or 'none' you must specify http or file as well.
"""
sys.exit()
logbytes = 15360
if len(sys.argv)>1:
mode = sys.argv[len(sys.argv)-1]
if sys.argv[1] == 'all':
logbytes = 0
if sys.argv[1] == 'none':
logbytes = -1
if mode.startswith('file'):
# file://
logs = mode[5:]
#if mode.lower().startswith('http'):
# pass
#else if mode.lower().startswith('usb'):
# pass
#else if mode.lower().startswith('sd'):
# pass
logs = lc.write_logs(logs, logbytes)
print 'Logs saved in %s' % logs
sent_ok = False
if len(sys.argv)>1:
mode = sys.argv[len(sys.argv)-1]
if mode.startswith('http'):
print "Trying to send the logs using HTTP (web)"
if len(mode) == 4:
print "No default log destination, aborting"
sys.exit(1)
else:
url = mode
if ls.http_post_logs(url, logs):
print "Logs were sent."
sent_ok = True
else:
print "FAILED to send logs."
if sent_ok:
os.remove(logs)
print "Logs were sent, tempfile deleted."