diff options
author | Eduardo Silva <edsiper@gmail.com> | 2007-11-02 01:22:09 (GMT) |
---|---|---|
committer | Eduardo Silva <edsiper@gmail.com> | 2007-11-02 01:22:09 (GMT) |
commit | 23c3e327250601565d0cfa44b2e4a1a816eeb45a (patch) | |
tree | 9b6a58d6024605778f1f3050da6dbf6fc5dad7af /logcollect.py | |
parent | 0fb9d1370f4db52d14c371aa2558e6c40ecdb659 (diff) |
Add collector menu (testing)
Diffstat (limited to 'logcollect.py')
-rw-r--r-- | logcollect.py | 523 |
1 files changed, 523 insertions, 0 deletions
diff --git a/logcollect.py b/logcollect.py new file mode 100644 index 0000000..e016036 --- /dev/null +++ b/logcollect.py @@ -0,0 +1,523 @@ +#!/usr/bin/env python +# Copyright (C) 2006-2007, Pascal Scheffers <pascal@scheffers.net> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + +# +# 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 + +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 _mfg_data(self, item): + """Return mfg data item from /ofw/mfg-data/""" + + if not os.path.exists('/ofw/mfg-data/'+item): + return '' + + v = self.__read_file('/ofw/mfg-data/'+item) + # Remove trailing 0 character, if any: + if v != '' and ord(v[len(v)-1]) == 0: + v = v[:len(v)-1] + + 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): + return self._mfg_data('BV') + + 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/activities/*.activity'): + s += os.path.basename(path) + '\n' + + for path in glob.glob('/home/olpc/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 + """ + + if archive=='': + archive = '/dev/shm/logs-%s.zip' % self._mp.laptop_serial_number() + # Oops - null character in serialno! + #archive = '/dev/shm/logs.zip' + + z = zipfile.ZipFile(archive, 'w', zipfile.ZIP_DEFLATED) + + try: + z.writestr('info.txt', self.laptop_info()) + + if logbytes > -1: + # Include some log files from /var/log. + for fn in ['dmesg', 'messages', 'cron', 'maillog','rpmpkgs', + 'Xorg.0.log', 'spooler']: + 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)) + + # Include all current ones from sugar/logs + for path in glob.glob('/home/olpc/.sugar/default/logs/*.log'): + if os.access(path, os.F_OK): + if logbytes == 0: + z.write(path, 'sugar-logs/'+os.path.basename(path)) + else: + z.writestr('sugar-logs/'+os.path.basename(path), + self.file_tail(path, logbytes)) + + z.write('/etc/resolv.conf') + 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 = '' + + # Do not include UUID! + s += 'laptop-info-version: 1.0\n' + s += 'clock: %f\n' % time.clock() + s += 'date: %s' % 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() + + 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('log-collect.py') or \ + sys.argv[0].endswith('log-collect'): + print 'log-collect utility 1.0' + + lc = LogCollect() + ls = LogSend() + + logs = '' + mode = 'ask' + + if len(sys.argv)==1: + print """log-collect.py - send your XO logs to OLPC + +Usage: + log-collect.py http - send logs to default server + + log-collect.py http://server.name/submit.php + - submit logs to alternative server + + log-collect.py file:/media/xxxx-yyyy/mylog.zip + - save the zip file on a USB device or SD card + + log-collect.py all file:/media/xxxx-yyyy/mylog.zip + - Save to zip file and include ALL logs + + log-collect.py none http + - Just send info.txt, but no logs via http. + + """ + 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 + mode = 'ask' + 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: + url = 'http://pascal.scheffers.net/olpc/submit.tcl' + 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." + + |